mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
384 lines
14 KiB
TypeScript
384 lines
14 KiB
TypeScript
import EventEmitter from 'events';
|
|
import fetch from 'cross-fetch';
|
|
import Redis, { ReplyError } from 'ioredis';
|
|
import type { App } from '../../src';
|
|
import type { RedisLocker } from '../../src/util/locking/RedisLocker';
|
|
import type { RedisReadWriteLock, RedisResourceLock } from '../../src/util/locking/scripts/RedisLuaScripts';
|
|
import { REDIS_LUA_SCRIPTS } from '../../src/util/locking/scripts/RedisLuaScripts';
|
|
import { describeIf, getPort } from '../util/Util';
|
|
import { getDefaultVariables, getTestConfigPath, instantiateFromConfig } from './Config';
|
|
/**
|
|
* Test the general functionality of the server using a RedisLocker with Read-Write strategy.
|
|
*/
|
|
describeIf('docker')('A server with a RedisLocker', (): void => {
|
|
const port = getPort('RedisLocker');
|
|
const baseUrl = `http://localhost:${port}/`;
|
|
let app: App;
|
|
let locker: RedisLocker;
|
|
|
|
beforeAll(async(): Promise<void> => {
|
|
const instances = await instantiateFromConfig(
|
|
'urn:solid-server:test:Instances',
|
|
getTestConfigPath('server-redis-lock.json'),
|
|
getDefaultVariables(port, baseUrl),
|
|
) as Record<string, any>;
|
|
({ app, locker } = instances);
|
|
await app.start();
|
|
});
|
|
|
|
afterAll(async(): Promise<void> => {
|
|
await app.stop();
|
|
});
|
|
|
|
describe('has a locker that', (): void => {
|
|
it('can add a file to the store, read it and delete it.', async(): Promise<void> => {
|
|
// Create file
|
|
const fileUrl = `${baseUrl}testfile2.txt`;
|
|
const fileData = 'TESTFILE2';
|
|
|
|
let response = await fetch(fileUrl, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'content-type': 'text/plain',
|
|
},
|
|
body: fileData,
|
|
});
|
|
expect(response.status).toBe(201);
|
|
|
|
// Get file
|
|
response = await fetch(fileUrl);
|
|
expect(response.status).toBe(200);
|
|
const body = await response.text();
|
|
expect(body).toContain('TESTFILE2');
|
|
|
|
// DELETE file
|
|
response = await fetch(fileUrl, { method: 'DELETE' });
|
|
expect(response.status).toBe(205);
|
|
response = await fetch(fileUrl);
|
|
expect(response.status).toBe(404);
|
|
});
|
|
|
|
it('can create a folder and delete it.', async(): Promise<void> => {
|
|
const containerPath = 'secondfolder/';
|
|
const containerUrl = `${baseUrl}${containerPath}`;
|
|
// PUT
|
|
let response = await fetch(containerUrl, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'content-type': 'text/plain',
|
|
},
|
|
});
|
|
expect(response.status).toBe(201);
|
|
|
|
// GET
|
|
response = await fetch(containerUrl);
|
|
expect(response.status).toBe(200);
|
|
|
|
// DELETE
|
|
response = await fetch(containerUrl, { method: 'DELETE' });
|
|
expect(response.status).toBe(205);
|
|
response = await fetch(containerUrl);
|
|
expect(response.status).toBe(404);
|
|
});
|
|
|
|
it('can get a resource multiple times.', async(): Promise<void> => {
|
|
const fileUrl = `${baseUrl}image.png`;
|
|
const fileData = 'testtesttest';
|
|
|
|
let response = await fetch(fileUrl, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'content-type': 'text/plain',
|
|
},
|
|
body: fileData,
|
|
});
|
|
expect(response.status).toBe(201);
|
|
|
|
// GET 4 times
|
|
for (let i = 0; i < 4; i++) {
|
|
const res = await fetch(fileUrl);
|
|
expect(res.status).toBe(200);
|
|
expect(res.headers.get('content-type')).toBe('text/plain');
|
|
const body = await res.text();
|
|
expect(body).toContain('testtesttest');
|
|
}
|
|
|
|
// DELETE
|
|
response = await fetch(fileUrl, { method: 'DELETE' });
|
|
expect(response.status).toBe(205);
|
|
response = await fetch(fileUrl);
|
|
expect(response.status).toBe(404);
|
|
});
|
|
});
|
|
|
|
describe('implements ResoureLocker and', (): void => {
|
|
const identifier = { path: 'http://test.com/foo' };
|
|
|
|
it('can acquire a resource.', async(): Promise<void> => {
|
|
await expect(locker.acquire(identifier)).resolves.toBeUndefined();
|
|
// Clean up lock
|
|
await locker.release(identifier);
|
|
});
|
|
|
|
it('can release a resource.', async(): Promise<void> => {
|
|
await expect(locker.acquire(identifier)).resolves.toBeUndefined();
|
|
await expect(locker.release(identifier)).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('can acquire different locks simultaneously.', async(): Promise<void> => {
|
|
const lock1 = locker.acquire({ path: 'path1' });
|
|
const lock2 = locker.acquire({ path: 'path2' });
|
|
const lock3 = locker.acquire({ path: 'path3' });
|
|
const release1 = locker.release({ path: 'path1' });
|
|
const release2 = locker.release({ path: 'path2' });
|
|
const release3 = locker.release({ path: 'path3' });
|
|
|
|
await expect(Promise.all([ lock1, lock2, lock3 ])).resolves.toBeDefined();
|
|
// Clean up locks
|
|
await Promise.all([ release1, release2, release3 ]);
|
|
});
|
|
|
|
it('cannot acquire the same lock simultaneously.', async(): Promise<void> => {
|
|
await expect(locker.acquire(identifier)).resolves.toBeUndefined();
|
|
await expect(locker.acquire(identifier)).rejects
|
|
.toThrow(/The operation did not succeed after the set maximum of tries \(\d+\)./u);
|
|
await expect(locker.acquire(identifier)).rejects
|
|
.toThrow(/The operation did not succeed after the set maximum of tries \(\d+\)./u);
|
|
});
|
|
});
|
|
|
|
describe('implements ReadWriteLocker and', (): void => {
|
|
const identifier = { path: 'http://test.com/foo' };
|
|
|
|
it('can read a resource.', async(): Promise<void> => {
|
|
const testFn = jest.fn();
|
|
await expect(locker.withReadLock(identifier, (): any => testFn())).resolves.toBeUndefined();
|
|
expect(testFn).toHaveBeenCalled();
|
|
});
|
|
|
|
it('can write a resource.', async(): Promise<void> => {
|
|
const testFn = jest.fn();
|
|
await expect(locker.withWriteLock(identifier, (): any => testFn())).resolves.toBeUndefined();
|
|
expect(testFn).toHaveBeenCalled();
|
|
});
|
|
|
|
it('can read a resource twice again after it was unlocked.', async(): Promise<void> => {
|
|
const testFn = jest.fn();
|
|
await expect(locker.withReadLock(identifier, (): any => testFn())).resolves.toBeUndefined();
|
|
expect(testFn).toHaveBeenCalledTimes(1);
|
|
await expect(locker.withReadLock(identifier, (): any => testFn())).resolves.toBeUndefined();
|
|
expect(testFn).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('can acquire different readLocks simultaneously.', async(): Promise<void> => {
|
|
const testFn = jest.fn();
|
|
const lock1 = locker.withReadLock({ path: 'path1' }, (): any => testFn());
|
|
const lock2 = locker.withReadLock({ path: 'path2' }, (): any => testFn());
|
|
const lock3 = locker.withReadLock({ path: 'path3' }, (): any => testFn());
|
|
|
|
await expect(Promise.all([ lock1, lock2, lock3 ])).resolves.toBeDefined();
|
|
});
|
|
|
|
it('can acquire different writeLocks simultaneously.', async(): Promise<void> => {
|
|
const testFn = jest.fn();
|
|
const lock1 = locker.withWriteLock({ path: 'path1' }, (): any => testFn());
|
|
const lock2 = locker.withWriteLock({ path: 'path2' }, (): any => testFn());
|
|
const lock3 = locker.withWriteLock({ path: 'path3' }, (): any => testFn());
|
|
|
|
await expect(Promise.all([ lock1, lock2, lock3 ])).resolves.toBeDefined();
|
|
});
|
|
|
|
it('can acquire the same readLock simultaneously.', async(): Promise<void> => {
|
|
let res = '';
|
|
let countdown = 3;
|
|
const releaseSignal = new EventEmitter();
|
|
releaseSignal.on('countdown', (): void => {
|
|
countdown -= 1;
|
|
// Start releasing locks after 3 inits of the promises below
|
|
if (countdown === 0) {
|
|
[ 1, 0, 2 ].forEach((num): unknown => releaseSignal.emit(`release${num}`));
|
|
}
|
|
});
|
|
const promises = [ 0, 1, 2 ].map((num): Promise<any> =>
|
|
locker.withReadLock(identifier, async(): Promise<void> => {
|
|
res += `l${num}`;
|
|
await new Promise<void>((resolve): any => {
|
|
releaseSignal.on(`release${num}`, resolve);
|
|
releaseSignal.emit('countdown');
|
|
});
|
|
res += `r${num}`;
|
|
}));
|
|
|
|
await Promise.all(promises);
|
|
expect(res).toBe('l0l1l2r1r0r2');
|
|
});
|
|
|
|
it('cannot acquire the same writeLock simultaneously.', async(): Promise<void> => {
|
|
let res = '';
|
|
|
|
await expect(locker.withWriteLock(identifier, async(): Promise<void> => {
|
|
res += 'l0';
|
|
await expect(locker.withWriteLock(identifier, (): void => {
|
|
res += 'l1';
|
|
res += 'r1';
|
|
})).rejects.toThrow();
|
|
res += 'r0';
|
|
})).resolves.toBeUndefined();
|
|
|
|
await expect(locker.withWriteLock(identifier, async(): Promise<void> => {
|
|
res += 'l2';
|
|
res += 'r2';
|
|
})).resolves.toBeUndefined();
|
|
|
|
expect(res).toBe('l0r0l2r2');
|
|
});
|
|
});
|
|
|
|
describe('defines custom Redis lua functions', (): void => {
|
|
let redis: Redis & RedisReadWriteLock & RedisResourceLock;
|
|
|
|
async function clearRedis(): Promise<void> {
|
|
const keys = await redis.keys('*');
|
|
if (keys && keys.length > 0) {
|
|
await redis.del(keys);
|
|
}
|
|
}
|
|
|
|
beforeAll(async(): Promise<void> => {
|
|
redis = new Redis('127.0.0.1:6379') as Redis & RedisReadWriteLock & RedisResourceLock;
|
|
|
|
// Register lua scripts
|
|
for (const [ name, script ] of Object.entries(REDIS_LUA_SCRIPTS)) {
|
|
redis.defineCommand(name, { numberOfKeys: 1, lua: script });
|
|
}
|
|
|
|
await clearRedis();
|
|
});
|
|
|
|
afterAll(async(): Promise<void> => {
|
|
await redis.quit();
|
|
});
|
|
|
|
beforeEach(async(): Promise<void> => {
|
|
await clearRedis();
|
|
});
|
|
|
|
it('#acquireReadLock.', async(): Promise<void> => {
|
|
const key1 = 'key1';
|
|
const writeKey1 = `${key1}.wlock`;
|
|
const countKey1 = `${key1}.count`;
|
|
// Test fails
|
|
await redis.set(writeKey1, 'locked');
|
|
await expect(redis.acquireReadLock(key1)).resolves.toBe(0);
|
|
|
|
// Test succeeds
|
|
await redis.del(writeKey1);
|
|
await expect(redis.acquireReadLock(key1)).resolves.toBe(1);
|
|
await expect(redis.get(countKey1)).resolves.toBe('1');
|
|
await expect(redis.acquireReadLock(key1)).resolves.toBe(1);
|
|
await expect(redis.get(countKey1)).resolves.toBe('2');
|
|
await expect(redis.acquireReadLock(key1)).resolves.toBe(1);
|
|
await expect(redis.get(countKey1)).resolves.toBe('3');
|
|
});
|
|
|
|
it('#acquireWriteLock.', async(): Promise<void> => {
|
|
const key1 = 'key1';
|
|
const writeKey1 = `${key1}.wlock`;
|
|
const countKey1 = `${key1}.count`;
|
|
|
|
// Test fails because count > 0
|
|
await expect(redis.incr(countKey1)).resolves.toBe(1);
|
|
await expect(redis.acquireWriteLock(key1)).resolves.toBe(0);
|
|
|
|
// Test fails because write lock is present
|
|
await clearRedis();
|
|
await redis.set(writeKey1, 'locked');
|
|
await expect(redis.acquireWriteLock(key1)).resolves.toBe(0);
|
|
|
|
// Test succeeds
|
|
await clearRedis();
|
|
await expect(redis.acquireWriteLock(key1)).resolves.toBe('OK');
|
|
|
|
// Test fails again
|
|
await expect(redis.acquireWriteLock(key1)).resolves.toBe(0);
|
|
});
|
|
|
|
it('#releaseReadLock.', async(): Promise<void> => {
|
|
const key1 = 'key1';
|
|
const countKey1 = `${key1}.count`;
|
|
|
|
// Test succeeds
|
|
await expect(redis.acquireReadLock(key1)).resolves.toBe(1);
|
|
await expect(redis.acquireReadLock(key1)).resolves.toBe(1);
|
|
await expect(redis.acquireReadLock(key1)).resolves.toBe(1);
|
|
await expect(redis.get(countKey1)).resolves.toBe('3');
|
|
|
|
await expect(redis.releaseReadLock(key1)).resolves.toBe(1);
|
|
await expect(redis.releaseReadLock(key1)).resolves.toBe(1);
|
|
await expect(redis.releaseReadLock(key1)).resolves.toBe(1);
|
|
await expect(redis.get(countKey1)).resolves.toBe('0');
|
|
|
|
// Test fails
|
|
await expect(redis.releaseReadLock(key1)).rejects
|
|
.toThrow(ReplyError);
|
|
await expect(redis.releaseReadLock(key1)).rejects
|
|
.toThrow('Error trying to release readlock when read count was 0.');
|
|
});
|
|
|
|
it('#releaseWriteLock.', async(): Promise<void> => {
|
|
const key1 = 'key1';
|
|
const writeKey1 = `${key1}.wlock`;
|
|
|
|
// Test fails
|
|
await expect(redis.releaseWriteLock(key1)).rejects
|
|
.toThrow(ReplyError);
|
|
await expect(redis.releaseWriteLock(key1)).rejects
|
|
.toThrow('Error trying to release writelock that did not exist.');
|
|
|
|
// Test succeeds
|
|
await redis.acquireWriteLock(key1);
|
|
await expect(redis.exists(writeKey1)).resolves.toBe(1);
|
|
await expect(redis.get(writeKey1)).resolves.toBe('locked');
|
|
await expect(redis.releaseWriteLock(key1)).resolves.toBe(1);
|
|
await expect(redis.exists(writeKey1)).resolves.toBe(0);
|
|
});
|
|
|
|
it('#acquireLock.', async(): Promise<void> => {
|
|
const key1 = 'key1';
|
|
const lockKey1 = `${key1}.lock`;
|
|
// Test succeeds
|
|
await expect(redis.acquireLock(key1)).resolves.toBe('OK');
|
|
await expect(redis.exists(lockKey1)).resolves.toBe(1);
|
|
await expect(redis.get(lockKey1)).resolves.toBe('locked');
|
|
|
|
// Test fails
|
|
await expect(redis.acquireLock(key1)).resolves.toBe(0);
|
|
await expect(redis.exists(lockKey1)).resolves.toBe(1);
|
|
await expect(redis.get(lockKey1)).resolves.toBe('locked');
|
|
|
|
// Test succeeds again
|
|
await redis.releaseLock(key1);
|
|
await expect(redis.exists(lockKey1)).resolves.toBe(0);
|
|
await expect(redis.acquireLock(key1)).resolves.toBe('OK');
|
|
});
|
|
|
|
it('#releaseLock.', async(): Promise<void> => {
|
|
const key1 = 'key1';
|
|
const lockKey1 = `${key1}.lock`;
|
|
|
|
// Test fails
|
|
await expect(redis.releaseLock(key1)).rejects
|
|
.toThrow(ReplyError);
|
|
await expect(redis.releaseLock(key1)).rejects
|
|
.toThrow('Error trying to release lock that did not exist.');
|
|
|
|
// Test succeeds
|
|
await redis.acquireLock(key1);
|
|
await expect(redis.exists(lockKey1)).resolves.toBe(1);
|
|
await expect(redis.get(lockKey1)).resolves.toBe('locked');
|
|
await expect(redis.releaseLock(key1)).resolves.toBe(1);
|
|
await expect(redis.exists(lockKey1)).resolves.toBe(0);
|
|
});
|
|
});
|
|
});
|