import { EventEmitter } from 'events'; import { promises as fs } from 'fs'; import type { IncomingHttpHeaders } from 'http'; import { join } from 'path'; import * as url from 'url'; import type { MockResponse } from 'node-mocks-http'; import { createResponse } from 'node-mocks-http'; import streamifyArray from 'streamify-array'; import type { ResourceStore } from '../../index'; import { RepresentationMetadata } from '../../index'; import type { PermissionSet } from '../../src/ldp/permissions/PermissionSet'; import type { HttpHandler } from '../../src/server/HttpHandler'; import type { HttpRequest } from '../../src/server/HttpRequest'; import { CONTENT_TYPE } from '../../src/util/UriConstants'; import { call } from './Util'; export class AclTestHelper { public readonly store: ResourceStore; public id: string; public constructor(store: ResourceStore, id: string) { this.store = store; this.id = id; } public async setSimpleAcl( permissions: PermissionSet, agentClass: 'agent' | 'authenticated', ): Promise { const acl: string[] = [ '@prefix acl: .\n', '@prefix foaf: .\n', ' a acl:Authorization', ]; for (const perm of [ 'Read', 'Append', 'Write', 'Delete' ]) { if (permissions[perm.toLowerCase() as keyof PermissionSet]) { acl.push(`;\n acl:mode acl:${perm}`); } } acl.push(';\n acl:mode acl:Control'); acl.push(`;\n acl:accessTo <${this.id}>`); acl.push(`;\n acl:default <${this.id}>`); acl.push( `;\n acl:agentClass ${ agentClass === 'agent' ? 'foaf:Agent' : 'foaf:AuthenticatedAgent' }`, ); acl.push('.'); const representation = { binary: true, data: streamifyArray(acl), metadata: new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' }), }; return this.store.setRepresentation( { path: `${this.id}.acl` }, representation, ); } } export class FileTestHelper { public readonly handler: HttpHandler; public readonly baseUrl: URL; public constructor(handler: HttpHandler, baseUrl: URL) { this.handler = handler; this.baseUrl = baseUrl; } public async simpleCall( requestUrl: URL, method: string, headers: IncomingHttpHeaders, ): Promise> { return call(this.handler, requestUrl, method, headers, []); } public async callWithFile( requestUrl: URL, method: string, headers: IncomingHttpHeaders, data: Buffer, ): Promise> { const request = streamifyArray([ data ]) as HttpRequest; request.url = requestUrl.pathname; request.method = method; request.headers = headers; request.headers.host = requestUrl.host; const response: MockResponse = createResponse({ eventEmitter: EventEmitter, }); const endPromise = new Promise((resolve): void => { response.on('end', (): void => { expect(response._isEndCalled()).toBeTruthy(); resolve(); }); }); await this.handler.handleSafe({ request, response }); await endPromise; return response; } public async createFile(fileLocation: string, slug: string, contentType: string, mayFail = false): Promise> { const fileData = await fs.readFile( join(__dirname, fileLocation), ); const response: MockResponse = await this.callWithFile( this.baseUrl, 'POST', { 'content-type': contentType, slug, 'transfer-encoding': 'chunked' }, fileData, ); if (!mayFail) { expect(response.statusCode).toBe(200); expect(response._getData()).toHaveLength(0); expect(response._getHeaders().location).toContain(url.format(this.baseUrl)); } return response; } public async overwriteFile(fileLocation: string, requestUrl: string, contentType: string): Promise> { const fileData = await fs.readFile( join(__dirname, fileLocation), ); const putUrl = new URL(requestUrl); const response: MockResponse = await this.callWithFile( putUrl, 'PUT', { 'content-type': contentType, 'transfer-encoding': 'chunked' }, fileData, ); expect(response.statusCode).toBe(200); expect(response._getData()).toHaveLength(0); expect(response._getHeaders().location).toContain(url.format(putUrl)); return response; } public async getFile(requestUrl: string): Promise> { const getUrl = new URL(requestUrl); const response = await this.simpleCall(getUrl, 'GET', { accept: '*/*' }); expect(response.statusCode).toBe(200); return response; } public async deleteFile(requestUrl: string, mayFail = false): Promise> { const deleteUrl = new URL(requestUrl); const response = await this.simpleCall(deleteUrl, 'DELETE', {}); if (!mayFail) { expect(response.statusCode).toBe(200); expect(response._getData()).toHaveLength(0); expect(response._getHeaders().location).toBe(url.format(requestUrl)); } return response; } public async createFolder(slug: string): Promise> { const response: MockResponse = await this.simpleCall( this.baseUrl, 'POST', { slug, link: '; rel="type"', 'content-type': 'text/turtle', 'transfer-encoding': 'chunked', }, ); expect(response.statusCode).toBe(200); expect(response._getData()).toHaveLength(0); expect(response._getHeaders().location).toContain(url.format(this.baseUrl)); return response; } public async getFolder(requestUrl: string): Promise> { const getUrl = new URL(requestUrl); // `n-quads` allow for easy testing if a triple is present return await this.simpleCall(getUrl, 'GET', { accept: 'application/n-quads' }); } public async deleteFolder(requestUrl: string): Promise> { const deleteUrl = new URL(requestUrl); const response = await this.simpleCall(deleteUrl, 'DELETE', {}); expect(response.statusCode).toBe(200); expect(response._getData()).toHaveLength(0); expect(response._getHeaders().location).toBe(url.format(requestUrl)); return response; } public async shouldNotExist(requestUrl: string): Promise> { const getUrl = new URL(requestUrl); const response = await this.simpleCall(getUrl, 'GET', { accept: '*/*' }); expect(response.statusCode).toBe(404); expect(response._getData()).toContain('NotFoundHttpError'); return response; } }