Automated browser testing (#1415)

* Move automated api tests to api directory

* First pass at automated browser testing
This commit is contained in:
Gabe Kangas
2021-09-17 14:04:09 -07:00
committed by GitHub
parent 5fc8465746
commit cc6b257470
29 changed files with 9094 additions and 25 deletions

View File

@@ -0,0 +1,13 @@
var request = require('supertest');
request = request('http://127.0.0.1:8080');
test('correct number of log entries exist', (done) => {
request.get('/api/admin/logs').auth('admin', 'abc123').expect(200)
.then((res) => {
// expect(res.body).toHaveLength(8);
done();
});
});

View File

@@ -0,0 +1,40 @@
const { test } = require('@jest/globals');
var request = require('supertest');
request = request('http://127.0.0.1:8080');
const registerChat = require('./lib/chat').registerChat;
const sendChatMessage = require('./lib/chat').sendChatMessage;
var userDisplayName;
const message = Math.floor(Math.random() * 100) + ' test 123';
const testMessage = {
body: message,
type: 'CHAT',
};
test('can send a chat message', async (done) => {
const registration = await registerChat();
const accessToken = registration.accessToken;
userDisplayName = registration.displayName;
sendChatMessage(testMessage, accessToken, done);
});
test('can fetch chat messages', async (done) => {
const res = await request
.get('/api/admin/chat/messages')
.auth('admin', 'abc123')
.expect(200);
const expectedBody = `${testMessage.body}`
const message = res.body.filter(function (msg) {
return msg.body === expectedBody
})[0];
expect(message.body).toBe(expectedBody);
expect(message.user.displayName).toBe(userDisplayName);
expect(message.type).toBe(testMessage.type);
done();
});

View File

@@ -0,0 +1,42 @@
const { test } = require('@jest/globals');
var request = require('supertest');
request = request('http://127.0.0.1:8080');
const registerChat = require('./lib/chat').registerChat;
const sendChatMessage = require('./lib/chat').sendChatMessage;
const testVisibilityMessage = {
body: "message " + Math.floor(Math.random() * 100),
type: 'CHAT',
};
test('can send a chat message', async (done) => {
const registration = await registerChat();
const accessToken = registration.accessToken;
sendChatMessage(testVisibilityMessage, accessToken, done);
});
test('verify we can make API call to mark message as hidden', async (done) => {
const res = await request.get('/api/admin/chat/messages').auth('admin', 'abc123').expect(200)
const message = res.body[0];
const messageId = message.id;
await request.post('/api/admin/chat/updatemessagevisibility')
.auth('admin', 'abc123')
.send({ "idArray": [messageId], "visible": false }).expect(200);
done();
});
test('verify message has become hidden', async (done) => {
const res = await request.get('/api/admin/chat/messages')
.expect(200)
.auth('admin', 'abc123')
const message = res.body.filter(obj => {
return obj.body === `${testVisibilityMessage.body}`;
});
expect(message.length).toBe(1);
expect(message[0].hiddenAt).toBeTruthy();
done();
});

View File

@@ -0,0 +1,118 @@
const { test } = require('@jest/globals');
var request = require('supertest');
request = request('http://127.0.0.1:8080');
const WebSocket = require('ws');
const fs = require('fs');
const registerChat = require('./lib/chat').registerChat;
const sendChatMessage = require('./lib/chat').sendChatMessage;
const testVisibilityMessage = {
body: 'message ' + Math.floor(Math.random() * 100),
type: 'CHAT',
};
var userId;
var accessToken;
test('can register a user', async (done) => {
const registration = await registerChat();
userId = registration.id;
accessToken = registration.accessToken;
done();
});
test('can send a chat message', async (done) => {
sendChatMessage(testVisibilityMessage, accessToken, done);
});
test('can disable a user', async (done) => {
// To allow for visually being able to see the test hiding the
// message add a short delay.
await new Promise((r) => setTimeout(r, 1500));
await request
.post('/api/admin/chat/users/setenabled')
.send({ userId: userId, enabled: false })
.auth('admin', 'abc123')
.expect(200);
done();
});
test('verify user is disabled', async (done) => {
const response = await request
.get('/api/admin/chat/users/disabled')
.auth('admin', 'abc123')
.expect(200);
const tokenCheck = response.body.filter((user) => user.id === userId);
expect(tokenCheck).toHaveLength(1);
done();
});
test('verify messages from user are hidden', async (done) => {
const response = await request
.get('/api/admin/chat/messages')
.auth('admin', 'abc123')
.expect(200);
const message = response.body.filter((obj) => {
return obj.user.id === userId;
});
expect(message[0].hiddenAt).toBeTruthy();
done();
});
test('can re-enable a user', async (done) => {
await request
.post('/api/admin/chat/users/setenabled')
.send({ userId: userId, enabled: true })
.auth('admin', 'abc123')
.expect(200);
done();
});
test('verify user is enabled', async (done) => {
const response = await request
.get('/api/admin/chat/users/disabled')
.auth('admin', 'abc123')
.expect(200);
const tokenCheck = response.body.filter((user) => user.id === userId);
expect(tokenCheck).toHaveLength(0);
done();
});
test('verify user list is populated', async (done) => {
const ws = new WebSocket(
`ws://localhost:8080/ws?accessToken=${accessToken}`,
{
origin: 'http://localhost:8080',
}
);
ws.on('open', async function open() {
const response = await request
.get('/api/admin/chat/clients')
.auth('admin', 'abc123')
.expect(200);
expect(response.body.length).toBeGreaterThan(0);
// Optionally, if GeoIP is configured, check the location property.
if (fs.existsSync('../../data/GeoLite2-City.mmdb')) {
expect(response.body[0].geo.regionName).toBe('Localhost');
} else {
console.warn('GeoIP Data is not supplied. Skipping test. See https://owncast.online/docs/viewers/');
}
ws.close();
});
ws.on('error', function incoming(data) {
console.error(data);
ws.close();
});
ws.on('close', function incoming(data) {
done();
});
});

View File

@@ -0,0 +1,199 @@
var request = require('supertest');
request = request('http://127.0.0.1:8080');
const serverName = randomString();
const streamTitle = randomString();
const serverSummary = randomString();
const pageContent = `<p>${randomString()}</p>`;
const tags = [randomString(), randomString(), randomString()];
const latencyLevel = Math.floor(Math.random() * 4);
const streamOutputVariants = {
videoBitrate: randomNumber() * 100,
framerate: 42,
cpuUsageLevel: 2,
scaledHeight: randomNumber() * 100,
scaledWidth: randomNumber() * 100,
};
const socialHandles = [
{
url: 'http://facebook.org/' + randomString(),
platform: randomString(),
},
];
const s3Config = {
enabled: true,
endpoint: 'http://' + randomString(),
accessKey: randomString(),
secret: randomString(),
bucket: randomString(),
region: randomString(),
};
const forbiddenUsernames = [randomString(), randomString(), randomString()];
test('set server name', async (done) => {
const res = await sendConfigChangeRequest('name', serverName);
done();
});
test('set stream title', async (done) => {
const res = await sendConfigChangeRequest('streamtitle', streamTitle);
done();
});
test('set server summary', async (done) => {
const res = await sendConfigChangeRequest('serversummary', serverSummary);
done();
});
test('set extra page content', async (done) => {
const res = await sendConfigChangeRequest('pagecontent', pageContent);
done();
});
test('set tags', async (done) => {
const res = await sendConfigChangeRequest('tags', tags);
done();
});
test('set latency level', async (done) => {
const res = await sendConfigChangeRequest(
'video/streamlatencylevel',
latencyLevel
);
done();
});
test('set video stream output variants', async (done) => {
const res = await sendConfigChangeRequest('video/streamoutputvariants', [
streamOutputVariants,
]);
done();
});
test('set social handles', async (done) => {
const res = await sendConfigChangeRequest('socialhandles', socialHandles);
done();
});
test('set s3 configuration', async (done) => {
const res = await sendConfigChangeRequest('s3', s3Config);
done();
});
test('set forbidden usernames', async (done) => {
const res = await sendConfigChangeRequest('chat/forbiddenusernames', forbiddenUsernames);
done();
});
test('verify updated config values', async (done) => {
const res = await request.get('/api/config');
expect(res.body.name).toBe(serverName);
expect(res.body.streamTitle).toBe(streamTitle);
expect(res.body.summary).toBe(`<p>${serverSummary}</p>`);
expect(res.body.extraPageContent).toBe(pageContent);
expect(res.body.logo).toBe('/logo');
expect(res.body.socialHandles).toStrictEqual(socialHandles);
done();
});
// Test that the raw video details being broadcasted are coming through
test('stream details are correct', (done) => {
request
.get('/api/admin/status')
.auth('admin', 'abc123')
.expect(200)
.then((res) => {
expect(res.body.broadcaster.streamDetails.width).toBe(320);
expect(res.body.broadcaster.streamDetails.height).toBe(180);
expect(res.body.broadcaster.streamDetails.framerate).toBe(24);
expect(res.body.broadcaster.streamDetails.videoBitrate).toBe(1269);
expect(res.body.broadcaster.streamDetails.videoCodec).toBe('H.264');
expect(res.body.broadcaster.streamDetails.audioCodec).toBe('AAC');
expect(res.body.online).toBe(true);
done();
});
});
test('admin configuration is correct', (done) => {
request
.get('/api/admin/serverconfig')
.auth('admin', 'abc123')
.expect(200)
.then((res) => {
expect(res.body.instanceDetails.name).toBe(serverName);
expect(res.body.instanceDetails.summary).toBe(serverSummary);
expect(res.body.instanceDetails.tags).toStrictEqual(tags);
expect(res.body.instanceDetails.socialHandles).toStrictEqual(
socialHandles
);
expect(res.body.forbiddenUsernames).toStrictEqual(forbiddenUsernames);
expect(res.body.videoSettings.latencyLevel).toBe(latencyLevel);
expect(res.body.videoSettings.videoQualityVariants[0].framerate).toBe(
streamOutputVariants.framerate
);
expect(res.body.videoSettings.videoQualityVariants[0].cpuUsageLevel).toBe(
streamOutputVariants.cpuUsageLevel
);
expect(res.body.yp.enabled).toBe(false);
expect(res.body.streamKey).toBe('abc123');
expect(res.body.s3.enabled).toBe(s3Config.enabled);
expect(res.body.s3.endpoint).toBe(s3Config.endpoint);
expect(res.body.s3.accessKey).toBe(s3Config.accessKey);
expect(res.body.s3.secret).toBe(s3Config.secret);
expect(res.body.s3.bucket).toBe(s3Config.bucket);
expect(res.body.s3.region).toBe(s3Config.region);
done();
});
});
test('frontend configuration is correct', (done) => {
request
.get('/api/config')
.expect(200)
.then((res) => {
expect(res.body.name).toBe(serverName);
expect(res.body.logo).toBe('/logo');
expect(res.body.socialHandles).toStrictEqual(socialHandles);
done();
});
});
async function sendConfigChangeRequest(endpoint, value) {
const url = '/api/admin/config/' + endpoint;
const res = await request
.post(url)
.auth('admin', 'abc123')
.send({ value: value })
.expect(200);
expect(res.body.success).toBe(true);
return res;
}
async function sendConfigChangePayload(endpoint, payload) {
const url = '/api/admin/config/' + endpoint;
const res = await request
.post(url)
.auth('admin', 'abc123')
.send(payload)
.expect(200);
expect(res.body.success).toBe(true);
return res;
}
function randomString(length = 20) {
return Math.random().toString(16).substr(2, length);
}
function randomNumber() {
return Math.floor(Math.random() * 5);
}

View File

@@ -0,0 +1,10 @@
var request = require('supertest');
request = request('http://127.0.0.1:8080');
test('service is online', (done) => {
request.get('/api/status').expect(200)
.then((res) => {
expect(res.body.online).toBe(true);
done();
});
});

View File

@@ -0,0 +1,178 @@
var request = require('supertest');
request = request('http://127.0.0.1:8080');
var accessToken = '';
var webhookID;
const webhook = 'https://super.duper.cool.thing.biz/owncast';
const events = ['CHAT'];
test('create webhook', async (done) => {
const res = await sendIntegrationsChangePayload('webhooks/create', {
url: webhook,
events: events,
});
expect(res.body.url).toBe(webhook);
expect(res.body.timestamp).toBeTruthy();
expect(res.body.events).toStrictEqual(events);
done();
});
test('check webhooks', (done) => {
request
.get('/api/admin/webhooks')
.auth('admin', 'abc123')
.expect(200)
.then((res) => {
expect(res.body).toHaveLength(1);
expect(res.body[0].url).toBe(webhook);
expect(res.body[0].events).toStrictEqual(events);
webhookID = res.body[0].id;
done();
});
});
test('delete webhook', async (done) => {
const res = await sendIntegrationsChangePayload('webhooks/delete', {
id: webhookID,
});
expect(res.body.success).toBe(true);
done();
});
test('check that webhook was deleted', (done) => {
request
.get('/api/admin/webhooks')
.auth('admin', 'abc123')
.expect(200)
.then((res) => {
expect(res.body).toHaveLength(0);
done();
});
});
test('create access token', async (done) => {
const name = 'Automated integration test';
const scopes = [
'CAN_SEND_SYSTEM_MESSAGES',
'CAN_SEND_MESSAGES',
'HAS_ADMIN_ACCESS',
];
const res = await sendIntegrationsChangePayload('accesstokens/create', {
name: name,
scopes: scopes,
});
expect(res.body.accessToken).toBeTruthy();
expect(res.body.createdAt).toBeTruthy();
expect(res.body.displayName).toBe(name);
expect(res.body.scopes).toStrictEqual(scopes);
accessToken = res.body.accessToken;
done();
});
test('check access tokens', async (done) => {
const res = await request
.get('/api/admin/accesstokens')
.auth('admin', 'abc123')
.expect(200);
const tokenCheck = res.body.filter(
(token) => token.accessToken === accessToken
);
expect(tokenCheck).toHaveLength(1);
done();
});
test('send a system message using access token', async (done) => {
const payload = {
body: 'This is a test system message from the automated integration test',
};
const res = await request
.post('/api/integrations/chat/system')
.set('Authorization', 'Bearer ' + accessToken)
.send(payload)
.expect(200);
done();
});
test('send an external integration message using access token', async (done) => {
const payload = {
body: 'This is a test external message from the automated integration test',
};
const res = await request
.post('/api/integrations/chat/send')
.set('Authorization', 'Bearer ' + accessToken)
.send(payload)
.expect(200);
done();
});
test('send an external integration action using access token', async (done) => {
const payload = {
body: 'This is a test external action from the automated integration test',
};
const res = await request
.post('/api/integrations/chat/action')
.set('Authorization', 'Bearer ' + accessToken)
.send(payload)
.expect(200);
done();
});
test('test fetch chat history using access token', async (done) => {
const res = await request
.get('/api/integrations/chat')
.set('Authorization', 'Bearer ' + accessToken)
.expect(200);
done();
});
test('test fetch chat history failure using invalid access token', async (done) => {
const res = await request
.get('/api/integrations/chat')
.set('Authorization', 'Bearer ' + 'invalidToken')
.expect(401);
done();
});
test('test fetch chat history OPTIONS request', async (done) => {
const res = await request
.options('/api/integrations/chat')
.set('Authorization', 'Bearer ' + accessToken)
.expect(204);
done();
});
test('delete access token', async (done) => {
const res = await sendIntegrationsChangePayload('accesstokens/delete', {
token: accessToken,
});
expect(res.body.success).toBe(true);
done();
});
test('check token delete was successful', async (done) => {
const res = await request
.get('/api/admin/accesstokens')
.auth('admin', 'abc123')
.expect(200);
const tokenCheck = res.body.filter(
(token) => token.accessToken === accessToken
);
expect(tokenCheck).toHaveLength(0);
done();
});
async function sendIntegrationsChangePayload(endpoint, payload) {
const url = '/api/admin/' + endpoint;
const res = await request
.post(url)
.auth('admin', 'abc123')
.send(payload)
.expect(200);
return res;
}

View File

@@ -0,0 +1,33 @@
var request = require('supertest');
request = request('http://127.0.0.1:8080');
const WebSocket = require('ws');
async function registerChat() {
try {
const response = await request.post('/api/chat/register');
return response.body;
} catch (e) {
console.error(e);
}
}
function sendChatMessage(message, accessToken, done) {
const ws = new WebSocket(
`ws://localhost:8080/ws?accessToken=${accessToken}`,
{
origin: 'http://localhost:8080',
}
);
function onOpen() {
ws.send(JSON.stringify(message), function () {
ws.close();
done();
});
}
ws.on('open', onOpen);
}
module.exports.sendChatMessage = sendChatMessage;
module.exports.registerChat = registerChat;

11493
test/automated/api/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
{
"name": "owncast-test-automation",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest"
},
"author": "",
"license": "ISC",
"dependencies": {
"supertest": "^6.0.1",
"websocket": "^1.0.32"
},
"devDependencies": {
"jest": "^26.6.3"
}
}

43
test/automated/api/run.sh Executable file
View File

@@ -0,0 +1,43 @@
#!/bin/bash
TEMP_DB=$(mktemp)
# Install the node test framework
npm install --silent > /dev/null
# Download a specific version of ffmpeg
if [ ! -d "ffmpeg" ]; then
mkdir ffmpeg
pushd ffmpeg > /dev/null
curl -sL https://github.com/vot/ffbinaries-prebuilt/releases/download/v4.2.1/ffmpeg-4.2.1-linux-64.zip --output ffmpeg.zip > /dev/null
unzip -o ffmpeg.zip > /dev/null
PATH=$PATH:$(pwd)
popd > /dev/null
fi
pushd ../../.. > /dev/null
# Build and run owncast from source
go build -o owncast main.go pkged.go
./owncast -database $TEMP_DB &
SERVER_PID=$!
popd > /dev/null
sleep 5
# Start streaming the test file over RTMP to
# the local owncast instance.
ffmpeg -hide_banner -loglevel panic -stream_loop -1 -re -i ../test.mp4 -vcodec libx264 -profile:v main -sc_threshold 0 -b:v 1300k -acodec copy -f flv rtmp://127.0.0.1/live/abc123 &
FFMPEG_PID=$!
function finish {
rm $TEMP_DB
kill $SERVER_PID $FFMPEG_PID
}
trap finish EXIT
echo "Waiting..."
sleep 13
# Run the tests against the instance.
npm test