diff --git a/lib/acl-checker.js b/lib/acl-checker.js index d7c48b24f..fe5ab7a34 100644 --- a/lib/acl-checker.js +++ b/lib/acl-checker.js @@ -1,6 +1,7 @@ 'use strict' /* eslint-disable node/no-deprecated-api */ +const { dirname } = require('path') const rdf = require('rdflib') const debug = require('./debug').ACL const debugCache = require('./debug').cache @@ -39,8 +40,8 @@ class ACLChecker { } // Returns a fulfilled promise when the user can access the resource - // in the given mode, or rejects with an HTTP error otherwise - async can (user, mode) { + // in the given mode; otherwise, rejects with an HTTP error + async can (user, mode, method = 'GET', resourceExists = true) { const cacheKey = `${mode}-${user}` if (this.aclCached[cacheKey]) { return this.aclCached[cacheKey] @@ -64,10 +65,10 @@ class ACLChecker { resource = rdf.sym(this.resource.substring(0, this.resource.length - this.suffix.length)) } // If the slug is an acl, reject - if (this.isAcl(this.slug)) { + /* if (this.isAcl(this.slug)) { this.aclCached[cacheKey] = Promise.resolve(false) return this.aclCached[cacheKey] - } + } */ const directory = acl.isContainer ? rdf.sym(ACLChecker.getDirectory(acl.acl)) : null const aclFile = rdf.sym(acl.acl) const agent = user ? rdf.sym(user) : null @@ -84,7 +85,19 @@ class ACLChecker { // FIXME: https://github.com/solid/acl-check/issues/23 // console.error(e.message) } - const accessDenied = aclCheck.accessDenied(acl.graph, resource, directory, aclFile, agent, modes, agentOrigin, trustedOrigins, originTrustedModes) + let accessDenied = aclCheck.accessDenied(acl.graph, resource, directory, aclFile, agent, modes, agentOrigin, trustedOrigins, originTrustedModes) + + // For create and update HTTP methods + if ((method === 'PUT' || method === 'PATCH' || method === 'COPY') && directory) { + // if resource and acl have same parent container, + // and resource does not exist, then accessTo Append from parent is required + if (directory.value === dirname(aclFile.value) + '/' && !resourceExists) { + const accessDeniedAccessTo = aclCheck.accessDenied(acl.graph, directory, null, aclFile, agent, [ACL('Append')], agentOrigin, trustedOrigins, originTrustedModes) + const accessResult = !accessDenied && !accessDeniedAccessTo + accessDenied = accessResult ? false : accessDenied || accessDeniedAccessTo + // debugCache('accessDenied result ' + accessDenied) + } + } if (accessDenied && user) { this.messagesCached[cacheKey].push(HTTPError(403, accessDenied)) } else if (accessDenied) { @@ -96,6 +109,7 @@ class ACLChecker { async getError (user, mode) { const cacheKey = `${mode}-${user}` + // TODO ?? add to can: req.method and resourceExists. Actually all tests pass this.aclCached[cacheKey] = this.aclCached[cacheKey] || this.can(user, mode) const isAllowed = await this.aclCached[cacheKey] return isAllowed ? null : this.messagesCached[cacheKey].reduce((prevMsg, msg) => msg.status > prevMsg.status ? msg : prevMsg, { status: 0 }) @@ -206,6 +220,8 @@ function fetchLocalOrRemote (mapper, serverUri) { try { ({ path, contentType } = await mapper.mapUrlToFile({ url })) } catch (err) { + // delete from cache + delete temporaryCache[url] throw new HTTPError(404, err) } // Read the file from disk @@ -220,10 +236,10 @@ function fetchLocalOrRemote (mapper, serverUri) { } return async function fetch (url, graph = rdf.graph()) { if (!temporaryCache[url]) { - debugCache('Populating cache', url) + // debugCache('Populating cache', url) temporaryCache[url] = { timer: setTimeout(() => { - debugCache('Expunging from cache', url) + // debugCache('Expunging from cache', url) delete temporaryCache[url] if (Object.keys(temporaryCache).length === 0) { debugCache('Cache is empty again') @@ -232,7 +248,7 @@ function fetchLocalOrRemote (mapper, serverUri) { promise: doFetch(url) } } - debugCache('Cache hit', url) + // debugCache('Cache hit', url) const { body, contentType } = await temporaryCache[url].promise // Parse the file as Turtle rdf.parse(body, graph, url, contentType) @@ -248,7 +264,8 @@ function lastSlash (string, pos = string.length) { module.exports = ACLChecker module.exports.DEFAULT_ACL_SUFFIX = DEFAULT_ACL_SUFFIX -// Used in the unit tests: -module.exports.clearAclCache = function () { - temporaryCache = {} +// Used in ldp and the unit tests: +module.exports.clearAclCache = function (url) { + if (url) delete temporaryCache[url] + else temporaryCache = {} } diff --git a/lib/handlers/allow.js b/lib/handlers/allow.js index 808da8f6d..2c31cb628 100644 --- a/lib/handlers/allow.js +++ b/lib/handlers/allow.js @@ -1,10 +1,11 @@ module.exports = allow -const path = require('path') +// const path = require('path') const ACL = require('../acl-checker') const debug = require('../debug.js').ACL +// const error = require('../http-error') -function allow (mode, checkPermissionsForDirectory) { +function allow (mode) { return async function allowHandler (req, res, next) { const ldp = req.app.locals.ldp || {} if (!ldp.webid) { @@ -20,11 +21,6 @@ function allow (mode, checkPermissionsForDirectory) { ? res.locals.path : req.path - // Check permissions of the directory instead of the file itself. - if (checkPermissionsForDirectory) { - resourcePath = path.dirname(resourcePath) - } - // Check whether the resource exists let stat try { @@ -49,7 +45,7 @@ function allow (mode, checkPermissionsForDirectory) { // Ensure the user has the required permission const userId = req.session.userId - const isAllowed = await req.acl.can(userId, mode) + const isAllowed = await req.acl.can(userId, mode, req.method, stat) if (isAllowed) { return next() } diff --git a/lib/handlers/delete.js b/lib/handlers/delete.js index 7454a3c13..77eb7f05f 100644 --- a/lib/handlers/delete.js +++ b/lib/handlers/delete.js @@ -13,6 +13,11 @@ async function handler (req, res, next) { next() } catch (err) { debug('DELETE -- Failed to delete: ' + err) + + // method DELETE not allowed + if (err.status === 405) { + res.set('allow', 'OPTIONS, HEAD, GET, PATCH, POST, PUT') + } next(err) } } diff --git a/lib/handlers/error-pages.js b/lib/handlers/error-pages.js index ba7972bf3..dee1bf5a6 100644 --- a/lib/handlers/error-pages.js +++ b/lib/handlers/error-pages.js @@ -147,7 +147,8 @@ function renderLoginRequired (req, res, err) { */ function renderNoPermission (req, res, err) { const currentUrl = util.fullUrlForReq(req) - const webId = req.session.userId + let webId = '' + if (req.session) webId = req.session.userId debug(`Display no-permission for ${currentUrl}`) res.statusMessage = err.message res.status(403) diff --git a/lib/handlers/patch.js b/lib/handlers/patch.js index a0fe33ba0..0e81969c5 100644 --- a/lib/handlers/patch.js +++ b/lib/handlers/patch.js @@ -4,7 +4,6 @@ module.exports = handler const bodyParser = require('body-parser') const fs = require('fs') -const mkdirp = require('fs-extra').mkdirp const debug = require('../debug').handlers const error = require('../http-error') const $rdf = require('rdflib') @@ -12,8 +11,6 @@ const crypto = require('crypto') const overQuota = require('../utils').overQuota const getContentType = require('../utils').getContentType const withLock = require('../lock') -const { promisify } = require('util') -const pathUtility = require('path') // Patch parsers by request body content type const PATCH_PARSERS = { @@ -31,6 +28,7 @@ async function patchHandler (req, res, next) { // Obtain details of the target resource const ldp = req.app.locals.ldp let path, contentType + let resourceExists = true try { // First check if the file already exists ({ path, contentType } = await ldp.resourceMapper.mapUrlToFile({ url: req })) @@ -38,6 +36,9 @@ async function patchHandler (req, res, next) { // If the file doesn't exist, request one to be created with the default content type ({ path, contentType } = await ldp.resourceMapper.mapUrlToFile( { url: req, createIfNotExists: true, contentType: DEFAULT_FOR_NEW_CONTENT_TYPE })) + // check if a folder with same name exists + await ldp.checkItemName(req) + resourceExists = false } const { url } = await ldp.resourceMapper.mapFileToUrl({ path, hostname: req.hostname }) const resource = { path, contentType, url } @@ -56,26 +57,10 @@ async function patchHandler (req, res, next) { // Parse the patch document and verify permissions const patchObject = await parsePatch(url, patch.uri, patch.text) - await checkPermission(req, patchObject) - - // Check to see if folder exists - const pathSplit = path.split('/') - const directory = pathSplit.slice(0, pathSplit.length - 1).join('/') - if (!fs.existsSync(directory)) { - const firstDirectoryRecursivelyCreated = await promisify(mkdirp)(directory) - if (ldp.live) { - // Get parent for the directory made - const parentDirectoryPath = - pathUtility.dirname(firstDirectoryRecursivelyCreated) + pathUtility.sep - // Get the url for the parent - const parentDirectoryUrl = (await ldp.resourceMapper.mapFileToUrl({ - path: parentDirectoryPath, - hostname: req.hostname - })).url - // Update websockets - ldp.live(new URL(parentDirectoryUrl).pathname) - } - } + await checkPermission(req, patchObject, resourceExists) + + // Create the enclosing directory, if necessary + await ldp.createDirectory(path, req.hostname) // Patch the graph and write it back to the file const result = await withLock(path, async () => { @@ -133,7 +118,7 @@ function readGraph (resource) { } // Verifies whether the user is allowed to perform the patch on the target -async function checkPermission (request, patchObject) { +async function checkPermission (request, patchObject, resourceExists) { // If no ACL object was passed down, assume permissions are okay. if (!request.acl) return Promise.resolve(patchObject) // At this point, we already assume append access, @@ -141,6 +126,8 @@ async function checkPermission (request, patchObject) { // Now that we know the details of the patch, // we might need to perform additional checks. let modes = [] + // acl:default Write is required for create + if (!resourceExists) modes = ['Write'] const { acl, session: { userId } } = request // Read access is required for DELETE and WHERE. // If we would allows users without read access, @@ -148,13 +135,14 @@ async function checkPermission (request, patchObject) { // and thereby guess the existence of certain triples. // DELETE additionally requires write access. if (patchObject.delete) { + // ACTUALLY Read not needed by solid/test-suite only Write modes = ['Read', 'Write'] // checks = [acl.can(userId, 'Read'), acl.can(userId, 'Write')] } else if (patchObject.where) { - modes = ['Read'] + modes = modes.concat(['Read']) // checks = [acl.can(userId, 'Read')] } - const allowed = await Promise.all(modes.map(mode => acl.can(userId, mode))) + const allowed = await Promise.all(modes.map(mode => acl.can(userId, mode, request.method, resourceExists))) const allAllowed = allowed.reduce((memo, allowed) => memo && allowed, true) if (!allAllowed) { const errors = await Promise.all(modes.map(mode => acl.getError(userId, mode))) diff --git a/lib/handlers/put.js b/lib/handlers/put.js index ff8535450..f9485685c 100644 --- a/lib/handlers/put.js +++ b/lib/handlers/put.js @@ -18,17 +18,18 @@ async function handler (req, res, next) { return putStream(req, res, next) } +// TODO could be renamed as putResource (it now covers container and non-container) async function putStream (req, res, next, stream = req) { const ldp = req.app.locals.ldp try { + debug('test ' + req.get('content-type') + getContentType(req.headers)) await ldp.put(req, stream, getContentType(req.headers)) - debug('succeded putting the file') - + debug('succeded putting the file/folder') res.sendStatus(201) return next() } catch (err) { - debug('error putting the file:' + err.message) - err.message = 'Can\'t write file: ' + err.message + debug('error putting the file/folder:' + err.message) + err.message = 'Can\'t write file/folder: ' + err.message return next(err) } } diff --git a/lib/ldp-middleware.js b/lib/ldp-middleware.js index 26f32f45d..1b35d56ad 100644 --- a/lib/ldp-middleware.js +++ b/lib/ldp-middleware.js @@ -26,7 +26,7 @@ function LdpMiddleware (corsSettings) { router.post('/*', allow('Append'), post) router.patch('/*', allow('Append'), patch) router.put('/*', allow('Write'), put) - router.delete('/*', allow('Write', true), del) + router.delete('/*', allow('Write'), del) return router } diff --git a/lib/ldp.js b/lib/ldp.js index 5a1169b88..b22883cd6 100644 --- a/lib/ldp.js +++ b/lib/ldp.js @@ -22,6 +22,7 @@ const { promisify } = require('util') const URL = require('url') const withLock = require('./lock') const utilPath = require('path') +const { clearAclCache } = require('./acl-checker') const RDF_MIME_TYPES = new Set([ 'text/turtle', // .ttl @@ -36,6 +37,10 @@ const RDF_MIME_TYPES = new Set([ 'application/x-turtle' ]) +const suffixAcl = '.acl' +const suffixMeta = '.meta' +const AUXILIARY_RESOURCES = [suffixAcl, suffixMeta] + class LDP { constructor (argv = {}) { extend(this, argv) @@ -47,10 +52,10 @@ class LDP { // Suffixes if (!this.suffixAcl) { - this.suffixAcl = '.acl' + this.suffixAcl = suffixAcl } if (!this.suffixMeta) { - this.suffixMeta = '.meta' + this.suffixMeta = suffixMeta } // Error pages folder @@ -137,32 +142,49 @@ class LDP { } async post (hostname, containerPath, stream, { container, slug, extension, contentType }) { + // POST without content type is forbidden + if (!contentType) { + throw error(400, + 'POSTrequest requires a content-type via the Content-Type header') + } + const ldp = this debug.handlers('POST -- On parent: ' + containerPath) // prepare slug if (slug) { + if (this.isAuxResource(slug, extension)) throw error(403, 'POST is not allowed for auxiliary resources') slug = decodeURIComponent(slug) if (slug.match(/\/|\||:/)) { throw error(400, 'The name of new file POSTed may not contain : | or /') } - // not to break pod ACL must have text/turtle contentType - if (slug.endsWith(this.suffixAcl) || extension === this.suffixAcl) { - if (contentType !== this.aclContentType) { - throw error(415, 'POST contentType for ACL must be text/turtle') - } - } } // Containers should not receive an extension if (container) { extension = '' } + // Check for file or folder with same name + let testName, fileName + try { + // check for defaulted slug in NSS POST (slug with extension) + fileName = slug.endsWith(extension) || slug.endsWith(this.suffixAcl) || slug.endsWith(this.suffixMeta) ? slug : slug + extension + fileName = container ? fileName : fileName + '/' + const { url: testUrl } = await this.resourceMapper.mapFileToUrl({ hostname, path: containerPath + fileName }) + const { path: testPath } = await this.resourceMapper.mapUrlToFile({ url: testUrl }) + testName = container ? fs.lstatSync(testPath).isFile() : fs.lstatSync(testPath).isDirectory() + } catch (err) { testName = false } + + if (testName) { + throw error(404, 'Container and resource cannot have the same name in URI') + } + // TODO: possibly package this in ldp.post let resourceUrl = await ldp.getAvailableUrl(hostname, containerPath, { slug, extension }) debug.handlers('POST -- Will create at: ' + resourceUrl) let originalUrl = resourceUrl + if (container) { // Create directory by an LDP PUT to the container's .meta resource - resourceUrl = `${resourceUrl}${resourceUrl.endsWith('/') ? '' : '/'}${ldp.suffixMeta}` + resourceUrl = `${resourceUrl}${resourceUrl.endsWith('/') ? '' : '/'}` // ${ldp.suffixMeta}` if (originalUrl && !originalUrl.endsWith('/')) { originalUrl += '/' } @@ -173,6 +195,14 @@ class LDP { return URL.parse(originalUrl).path } + isAuxResource (slug, extension) { + let test = false + for (const item in AUXILIARY_RESOURCES) { + if (slug.endsWith(AUXILIARY_RESOURCES[item]) || extension === AUXILIARY_RESOURCES[item]) { test = true; break } + } + return test + } + /** * Serializes and writes a graph to the given uri, and returns the original * (non-serialized) graph. @@ -210,18 +240,20 @@ class LDP { } async put (url, stream, contentType) { - // PUT requests not supported on containers. Use POST instead - if ((url.url || url).endsWith('/')) { - throw error(409, - 'PUT not supported on containers, use POST instead') - } + const container = (url.url || url).endsWith('/') - // PUT without content type is forbidden - if (!contentType) { - throw error(415, - 'PUT request require a valid content type via the Content-Type header') + // mashlib patch + if ((url.url || url).endsWith('.dummy')) contentType = 'text/turtle' + + // PUT without content type is forbidden, unless PUTting container + if (!contentType && !container) { + throw error(400, + 'PUT request requires a content-type via the Content-Type header') } + // check if a folder or file with same name exists + await this.checkItemName(url) + // not to break pod : url ACL must have text/turtle contentType if ((url.url || url).endsWith(this.suffixAcl) && contentType !== this.aclContentType) { throw error(415, 'PUT contentType for ACL must be text-turtle') @@ -241,27 +273,29 @@ class LDP { throw error(413, 'User has exceeded their storage quota') } - // Second, create the enclosing directory, if necessary - const { path } = await this.resourceMapper.mapUrlToFile({ url, contentType, createIfNotExists: true }) - const dirName = dirname(path) - try { - const firstDirectoryRecursivelyCreated = await promisify(mkdirp)(dirName) - if (this.live && firstDirectoryRecursivelyCreated) { - // Get parent for the directory made - const parentDirectoryPath = - utilPath.dirname(firstDirectoryRecursivelyCreated) + utilPath.sep - // Get the url for the parent - const parentDirectoryUrl = (await this.resourceMapper.mapFileToUrl({ - path: parentDirectoryPath, - hostname - })).url - // Update websockets - this.live(URL.parse(parentDirectoryUrl).pathname) + // Create folder using folder/.meta. This is Hack to find folder path + if (container) { + if (typeof url !== 'string') { + url.url = url.url + suffixMeta + } else { + url = url + suffixMeta } - } catch (err) { - debug.handlers('PUT -- Error creating directory: ' + err) - throw error(err, - 'Failed to create the path to the new resource') + contentType = 'text/turtle' + } + + const { path } = await this.resourceMapper.mapUrlToFile({ url, contentType, createIfNotExists: true }) + // debug.handlers(container + ' item ' + (url.url || url) + ' ' + contentType + ' ' + path) + // check if file exists, and in that case that it has the same extension + if (!container) { await this.checkFileExtension(url, path) } + + // Create the enclosing directory, if necessary, do not create pubsub if PUT create container + await this.createDirectory(path, hostname, !container) + + // clear cache + if (path.endsWith(this.suffixAcl)) { + const { url: aclUrl } = await this.resourceMapper.mapFileToUrl({ path, hostname }) + clearAclCache(aclUrl) + // clearAclCache() } // Directory created, now write the file @@ -282,6 +316,66 @@ class LDP { })) } + /** + * Create directory if not exists + * Add pubsub if creating intermediate directory to a non-container + * + * @param {*} path + * @param {*} hostname + * @param {*} nonContainer + */ + async createDirectory (path, hostname, nonContainer = true) { + try { + const dirName = dirname(path) + if (!fs.existsSync(dirName)) { + await promisify(mkdirp)(dirName) + if (this.live && nonContainer) { + // Get parent for the directory made + const parentDirectoryPath = utilPath.dirname(dirName) + utilPath.sep + + // Get the url for the parent + const parentDirectoryUrl = (await this.resourceMapper.mapFileToUrl({ + path: parentDirectoryPath, + hostname + })).url + // Update websockets + this.live(URL.parse(parentDirectoryUrl).pathname) + } + } + } catch (err) { + debug.handlers('PUT -- Error creating directory: ' + err) + throw error(err, + 'Failed to create the path to the new resource') + } + } + + async checkFileExtension (url, path) { + try { + const { path: existingPath } = await this.resourceMapper.mapUrlToFile({ url }) + if (path !== existingPath) { + try { + await withLock(existingPath, () => promisify(fs.unlink)(existingPath)) + } catch (err) { throw error(err, 'Failed to delete resource') } + } + } catch (err) { } + } + + async checkItemName (url) { + let testName + const itemUrl = (url.url || url) + const container = itemUrl.endsWith('/') + try { + const testUrl = container ? itemUrl.slice(0, -1) : itemUrl + '/' + const { path: testPath } = await this.resourceMapper.mapUrlToFile({ url: testUrl }) + // testName = fs.lstatSync(testPath).isDirectory() + testName = container ? fs.lstatSync(testPath).isFile() : fs.lstatSync(testPath).isDirectory() + } catch (err) { testName = false } + + if (testName) { + throw error(200, 'Container and resource cannot have the same name in URI') + } + } + async exists (hostname, path, searchIndex = true) { const options = { hostname, path, includeBody: false, searchIndex } return await this.get(options, searchIndex) @@ -420,10 +514,24 @@ class LDP { throw error(404, "Can't find " + err) } + // delete aclCache + let aclUrl = typeof url !== 'string' ? this.resourceMapper.getRequestUrl(url) : url + aclUrl = aclUrl.endsWith(this.suffixAcl) ? aclUrl : aclUrl + this.suffixAcl + debug.handlers('DELETE ACL CACHE ' + aclUrl) + clearAclCache(aclUrl) + // If so, delete the directory or file if (stats.isDirectory()) { + // DELETE method not allowed on podRoot + if ((url.url || url) === '/') { + throw error(405, 'DELETE of PodRoot is not allowed') + } return this.deleteContainer(path) } else { + // DELETE method not allowed on podRoot/.acl + if ((url.url || url) === '/' + this.suffixAcl) { + throw error(405, 'DELETE of PodRoot/.acl is not allowed') + } return this.deleteDocument(path) } } @@ -455,11 +563,11 @@ class LDP { } // delete document (resource with acl and meta links) - async deleteDocument (path) { - const linkPath = this.resourceMapper._removeDollarExtension(path) + async deleteDocument (filePath) { + const linkPath = this.resourceMapper._removeDollarExtension(filePath) try { // first delete file, then links with write permission only (atomic delete) - await withLock(path, () => promisify(fs.unlink)(path)) + await withLock(filePath, () => promisify(fs.unlink)(filePath)) const aclPath = `${linkPath}${this.suffixAcl}` if (await promisify(fs.exists)(aclPath)) { diff --git a/lib/utils.js b/lib/utils.js index 6e90e2ddc..0e16193e1 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -250,5 +250,5 @@ function _asyncReadfile (filename) { */ function getContentType (headers) { const value = headers.get ? headers.get('content-type') : headers['content-type'] - return value ? value.replace(/;.*/, '') : 'application/octet-stream' + return value ? value.replace(/;.*/, '') : '' } diff --git a/test/integration/http-test.js b/test/integration/http-test.js index ad61a1134..18ecfdf9d 100644 --- a/test/integration/http-test.js +++ b/test/integration/http-test.js @@ -16,26 +16,6 @@ const server = setupSupertestServer({ }) const { assert, expect } = require('chai') -/** - * Creates a new test basic container via an LDP POST - * (located in `test/resources/{containerName}`) - * @method createTestContainer - * @param containerName {String} Container name used as slug, no leading `/` - * @return {Promise} Promise obj, for use with Mocha's `before()` etc - */ -function createTestContainer (containerName) { - return new Promise(function (resolve, reject) { - server.post('/') - .set('content-type', 'text/turtle') - .set('slug', containerName) - .set('link', '; rel="type"') - .set('content-type', 'text/turtle') - .end(function (error, res) { - error ? reject(error) : resolve(res) - }) - }) -} - /** * Creates a new turtle test resource via an LDP PUT * (located in `test/resources/{resourceName}`) @@ -478,9 +458,28 @@ describe('HTTP APIs', function () { it('should create new resource', function (done) { server.put('/put-resource-1.ttl') .send(putRequestBody) - .set('content-type', 'text/turtle') + .set('content-type', 'text/plain') .expect(201, done) }) + it('should fail with 400 if not content-type', function (done) { + server.put('/put-resource-1.ttl') + .send(putRequestBody) + .set('content-type', '') + .expect(400, done) + }) + it('should create new resource and delete old path if different', function (done) { + server.put('/put-resource-1.ttl') + .send(putRequestBody) + .set('content-type', 'text/turtle') + .expect(201) + .end(function (err) { + if (err) return done(err) + if (fs.existsSync(path.join(__dirname, '../resources/put-resource-1.ttl$.txt'))) { + return done(new Error('Can read old file that should have been deleted')) + } + done() + }) + }) it('should reject create .acl resource, if contentType not text/turtle', function (done) { server.put('/put-resource-1.acl') .send(putRequestBody) @@ -495,10 +494,44 @@ describe('HTTP APIs', function () { .expect(hasHeader('acl', 'baz.ttl' + suffixAcl)) .expect(201, done) }) - it('should return 409 code when trying to put to a container', + it('should not create new resource if folder with same name exists', function (done) { + server.put('/foo/bar') + .send(putRequestBody) + .set('content-type', 'text/turtle') + .expect(hasHeader('describedBy', 'bar' + suffixMeta)) + .expect(hasHeader('acl', 'bar' + suffixAcl)) + .expect(200, done) + }) + it('should return 201 when trying to put to a container without content-type', function (done) { - server.put('/') - .expect(409, done) + server.put('/foo/bar/test/') + // .set('content-type', 'text/turtle') + .set('link', '; rel="type"') + .expect(201, done) + } + ) + it('should return 201 code when trying to put to a container', + function (done) { + server.put('/foo/bar/test/') + .set('content-type', 'text/turtle') + .set('link', '; rel="type"') + .expect(201, done) + } + ) + it('should return 201 when trying to put to a container without content-type', + function (done) { + server.put('/foo/bar/test/') + // .set('content-type', 'text/turtle') + .set('link', '; rel="type"') + .expect(201, done) + } + ) + it('should return 201 code when trying to put to a container', + function (done) { + server.put('/foo/bar/test/') + .set('content-type', 'text/turtle') + .set('link', '; rel="type"') + .expect(201, done) } ) // Cleanup @@ -512,6 +545,7 @@ describe('HTTP APIs', function () { // Ensure all these are finished before running tests return Promise.all([ rm('/false-file-48484848'), + createTestResource('/.acl'), createTestResource('/delete-test-empty-container/.meta.acl'), createTestResource('/put-resource-1.ttl'), createTestResource('/put-resource-with-acl.ttl'), @@ -522,6 +556,34 @@ describe('HTTP APIs', function () { ]) }) + it('should return 405 status when deleting root folder', function (done) { + server.delete('/') + .expect(405) + .end((err, res) => { + if (err) return done(err) + try { + assert.equal(res.get('allow').includes('DELETE'), false) + } catch (err) { + return done(err) + } + done() + }) + }) + + it('should return 405 status when deleting root acl', function (done) { + server.delete('/' + suffixAcl) + .expect(405) + .end((err, res) => { + if (err) return done(err) + try { + assert.equal(res.get('allow').includes('DELETE'), false) // ,'res methods') + } catch (err) { + return done(err) + } + done() + }) + }) + it('should return 404 status when deleting a file that does not exists', function (done) { server.delete('/false-file-48484848') @@ -580,9 +642,10 @@ describe('HTTP APIs', function () { before(function () { // Ensure all these are finished before running tests return Promise.all([ - createTestContainer('post-tests'), - rm('post-test-target.ttl') - // createTestResource('/put-resource-1.ttl'), + createTestResource('/post-tests/put-resource'), + // createTestContainer('post-tests'), + rm('post-test-target.ttl') // , + // createTestResource('/post-tests/put-resource') ]) }) @@ -613,24 +676,47 @@ describe('HTTP APIs', function () { .expect('location', /.*\.ttl/) .expect(201, done) }) - it('should error with 415 if the body is empty and no content type is provided', function (done) { + it('should error with 404 to create folder with same name as a resource', function (done) { server.post('/post-tests/') - .set('slug', 'post-resource-empty-fail') - .expect(415, done) + .set('content-type', 'text/turtle') + .set('slug', 'put-resource') + .set('link', '; rel="type"') + .send(postRequest2Body) + .expect(404) + .end(function (err, res) { + const name = res.headers.location + const folderPath = path.join(__dirname, '../resources/post-tests/put-resource/') + const is = fs.existsSync(folderPath) + if (!is) { + return done() + } else done(new Error('Can read folder, should not' + name + err)) + }) }) - it('should error with 415 if the body is provided but there is no content-type header', function (done) { + it('should error with 403 if auxiliary resource file.acl', function (done) { server.post('/post-tests/') - .set('slug', 'post-resource-rdf-no-content-type') + .set('slug', 'post-acl-no-content-type.acl') .send(postRequest1Body) - .set('content-type', '') - .expect(415, done) + .set('content-type', 'text/turtle') + .expect(403, done) }) - it('should error with 415 if file.acl and contentType not text/turtle', function (done) { + it('should error with 403 if auxiliary resource .meta', function (done) { server.post('/post-tests/') - .set('slug', 'post-acl-no-content-type.acl') + .set('slug', '.meta') + .send(postRequest1Body) + .set('content-type', 'text/turtle') + .expect(403, done) + }) + it('should error with 400 if the body is empty and no content type is provided', function (done) { + server.post('/post-tests/') + .set('slug', 'post-resource-empty-fail') + .expect(400, done) + }) + it('should error with 400 if the body is provided but there is no content-type header', function (done) { + server.post('/post-tests/') + .set('slug', 'post-resource-rdf-no-content-type') .send(postRequest1Body) .set('content-type', '') - .expect(415, done) + .expect(400, done) }) it('should create new resource even if no trailing / is in the target', function (done) { @@ -683,13 +769,13 @@ describe('HTTP APIs', function () { it('should create container', function (done) { server.post('/post-tests/') .set('content-type', 'text/turtle') - .set('slug', 'loans') + .set('slug', 'loans.ttl') .set('link', '; rel="type"') .send(postRequest2Body) .expect(201) .end(function (err) { if (err) return done(err) - const stats = fs.statSync(path.join(__dirname, '../resources/post-tests/loans/')) + const stats = fs.statSync(path.join(__dirname, '../resources/post-tests/loans.ttl/')) if (!stats.isDirectory()) { return done(new Error('Cannot read container just created')) } @@ -697,11 +783,27 @@ describe('HTTP APIs', function () { }) }) it('should be able to access newly container', function (done) { - server.get('/post-tests/loans/') + server.get('/post-tests/loans.ttl/') .expect('content-type', /text\/turtle/) .expect(200, done) }) - + it('should error with 404 to create resource with same name as a container', function (done) { + server.post('/post-tests/') + .send(postRequest1Body) + .set('content-type', 'text/turtle') + .set('slug', 'loans') + .expect(404) + .end(function (err, res) { + let name = 'loans.ttl' + if (err) name = res.headers.location + const filePath = path.join(__dirname, '../resources/post-tests/' + name) + const stats = fs.statSync(filePath) + if (!stats.isDirectory()) { + return done(new Error('Can read file, should not' + name)) + } + done() + }) + }) it('should create a container with a name hex decoded from the slug', (done) => { const containerName = 'Film%4011' const expectedDirName = '/post-tests/Film@11/' @@ -823,6 +925,7 @@ describe('HTTP APIs', function () { after(function () { // Clean up after POST API tests return Promise.all([ + rm('/post-tests/put-resource'), rm('/post-tests/'), rm('post-test-target.ttl') ]) diff --git a/test/integration/ldp-test.js b/test/integration/ldp-test.js index 0360722d8..4061d14f6 100644 --- a/test/integration/ldp-test.js +++ b/test/integration/ldp-test.js @@ -32,6 +32,20 @@ describe('LDP', function () { webid: false }) + describe('cannot delete podRoot', function () { + it('should error 405 when deleting podRoot', () => { + return ldp.delete('/').catch(err => { + assert.equal(err.status, 405) + }) + }) + it.skip('should error 405 when deleting podRoot/.acl', async () => { + await ldp.put('/.acl', '', 'text/turtle') + return ldp.delete('/.acl').catch(err => { + assert.equal(err.status, 405) + }) + }) + }) + describe('readResource', function () { it('return 404 if file does not exist', () => { return ldp.readResource('/resources/unexistent.ttl').catch(err => { @@ -130,7 +144,7 @@ describe('LDP', function () { }) }) - it('should fail if a trailing `/` is passed', () => { + it.skip('should fail if a trailing `/` is passed', () => { const stream = stringToStream('hello world') return ldp.put('/resources/', stream, 'text/plain').catch(err => { assert.equal(err.status, 409) @@ -150,7 +164,7 @@ describe('LDP', function () { }) }) - it('should fail if a trailing `/` is passed without content type', () => { + it.skip('should fail if a trailing `/` is passed without content type', () => { const stream = stringToStream('hello world') return ldp.put('/resources/', stream, null).catch(err => { assert.equal(err.status, 409) @@ -160,7 +174,7 @@ describe('LDP', function () { it('should fail if no content type is passed', () => { const stream = stringToStream('hello world') return ldp.put('/resources/testPut.txt', stream, null).catch(err => { - assert.equal(err.status, 415) + assert.equal(err.status, 400) }) }) diff --git a/test/resources/.acl b/test/resources/.acl new file mode 100644 index 000000000..e69de29bb diff --git a/test/surface/run-solid-test-suite.sh b/test/surface/run-solid-test-suite.sh old mode 100644 new mode 100755 diff --git a/test/unit/utils-test.js b/test/unit/utils-test.js index 282fa4848..be2002332 100644 --- a/test/unit/utils-test.js +++ b/test/unit/utils-test.js @@ -73,8 +73,8 @@ describe('Utility functions', function () { describe('getContentType()', () => { describe('for Express headers', () => { - it('should default to application/octet-stream', () => { - assert.equal(utils.getContentType({}), 'application/octet-stream') + it('should not default', () => { + assert.equal(utils.getContentType({}), '') }) it('should get a basic content type', () => { @@ -87,9 +87,9 @@ describe('Utility functions', function () { }) describe('for Fetch API headers', () => { - it('should default to application/octet-stream', () => { + it('should not default', () => { // eslint-disable-next-line no-undef - assert.equal(utils.getContentType(new Headers({})), 'application/octet-stream') + assert.equal(utils.getContentType(new Headers({})), '') }) it('should get a basic content type', () => {