MXS-1300: Move the REST API tests back into the core

As the REST API is a part of the core, it is more appropriate for the
tests to reside there as well. Further refactoring of the testing needs to
be done to allow multiple components to use the same framework but with
different tests.
This commit is contained in:
Markus Mäkelä
2017-07-13 13:48:57 +03:00
parent a082871726
commit 01fa1a827a
22 changed files with 3 additions and 3 deletions

View File

@ -83,3 +83,5 @@ add_test(TestTrxCompare_Update test_trxcompare ${CMAKE_CURRENT_SOURCE_DIR}/../..
add_test(TestTrxCompare_MaxScale test_trxcompare ${CMAKE_CURRENT_SOURCE_DIR}/../../../query_classifier/test/maxscale.test)
add_test(TestJson testjson)
add_test(TestHttp testhttp)
add_subdirectory(rest-api)

View File

@ -0,0 +1,3 @@
add_custom_target(test_rest_api
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/test_rest_api.sh ${CMAKE_SOURCE_DIR}
WORKING_DIRECTORY ${CMAKE_BINARY_DIR})

View File

@ -0,0 +1,30 @@
#!/bin/bash
#
# This script is run after each test block. It kills the MaxScale process
# and cleans up the directories that contain generated files.
#
test -z "$MAXSCALE_DIR" && exit 1
maxscaledir=$MAXSCALE_DIR
for ((i=0;i<10;i++))
do
pkill maxscale || break
sleep 0.5
done
# If it wasn't dead before, now it is
pgrep maxscale && pkill -9 maxscale
# Remove created users
rm $maxscaledir/passwd
rm $maxscaledir/maxadmin-users
rm -r $maxscaledir/lib/maxscale
rm -r $maxscaledir/cache/maxscale
rm -r $maxscaledir/run/maxscale
mkdir -m 0755 -p $maxscaledir/lib/maxscale
mkdir -m 0755 -p $maxscaledir/cache/maxscale
mkdir -m 0755 -p $maxscaledir/run/maxscale

View File

@ -0,0 +1,24 @@
#!/bin/bash
#
# This script is run before each test block. It starts MaxScale and waits for it
# to become responsive.
#
maxscaledir=$MAXSCALE_DIR
test -z "$MAXSCALE_DIR" && exit 1
# Start MaxScale
$maxscaledir/bin/maxscale -df $maxscaledir/maxscale.cnf >& $maxscaledir/maxscale.output &
pid=$!
# Wait for MaxScale to start
for ((i=0;i<60;i++))
do
$maxscaledir/bin/maxadmin help >& /dev/null && break
sleep 0.1
done
# Give MaxScale some time to settle
sleep 1

View File

@ -0,0 +1,37 @@
version: '2'
services:
server1:
image: mariadb:10.1
network_mode: "host"
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: Y
volumes:
- ./sql/master:/docker-entrypoint-initdb.d
command: mysqld --log-bin=binlog --binlog-format=ROW --server-id=3000 --port=3000 --log-slave-updates
server2:
image: mariadb:10.1
network_mode: "host"
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: Y
volumes:
- ./sql/slave:/docker-entrypoint-initdb.d
command: mysqld --log-bin=binlog --binlog-format=ROW --server-id=3001 --port=3001 --log-slave-updates
server3:
image: mariadb:10.1
network_mode: "host"
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: Y
volumes:
- ./sql/slave:/docker-entrypoint-initdb.d
command: mysqld --log-bin=binlog --binlog-format=ROW --server-id=3002 --port=3002 --log-slave-updates
server4:
image: mariadb:10.1
network_mode: "host"
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: Y
volumes:
- ./sql/slave:/docker-entrypoint-initdb.d
command: mysqld --log-bin=binlog --binlog-format=ROW --server-id=3003 --port=3003 --log-slave-updates

View File

@ -0,0 +1,19 @@
{
"name": "rest-api-tests",
"version": "1.0.0",
"repository": "https://github.com/mariadb-corporation/MaxScale",
"description": "MaxScale REST API tests",
"scripts": {
"test": "mocha --timeout 30000 --slow 10000"
},
"author": "",
"license": "SEE LICENSE IN ../../../../LICENSE.txt",
"dependencies": {
"ajv": "^5.0.1",
"chai": "^3.5.0",
"chai-as-promised": "^6.0.0",
"mocha": "^3.3.0",
"request": "^2.81.0",
"request-promise-native": "^1.0.3"
}
}

View File

@ -0,0 +1,14 @@
RESET MASTER;
CREATE DATABASE test;
CREATE USER 'maxuser'@'127.0.0.1' IDENTIFIED BY 'maxpwd';
CREATE USER 'maxuser'@'%' IDENTIFIED BY 'maxpwd';
GRANT ALL ON *.* TO 'maxuser'@'127.0.0.1' WITH GRANT OPTION;
GRANT ALL ON *.* TO 'maxuser'@'%' WITH GRANT OPTION;
CREATE USER 'skysql'@'127.0.0.1' IDENTIFIED BY 'skysql';
CREATE USER 'skysql'@'%' IDENTIFIED BY 'skysql';
GRANT ALL ON *.* TO 'skysql'@'127.0.0.1' WITH GRANT OPTION;
GRANT ALL ON *.* TO 'skysql'@'%' WITH GRANT OPTION;
SET GLOBAL max_connections=10000;

View File

@ -0,0 +1,3 @@
CHANGE MASTER TO MASTER_HOST='127.0.0.1', MASTER_PORT=3000, MASTER_USER='maxuser', MASTER_PASSWORD='maxpwd', MASTER_LOG_POS=4, MASTER_LOG_FILE='binlog.000001', MASTER_CONNECT_RETRY=1;
START SLAVE;
SET GLOBAL max_connections=10000;

View File

@ -0,0 +1,116 @@
require("../utils.js")()
function set_auth(auth, value) {
return request.get(auth + host + "/maxscale")
.then(function(resp) {
var d = JSON.parse(resp)
d.data.attributes.parameters.admin_auth = value;
return request.patch(auth + host + "/maxscale", { json: d })
})
.then(function() {
return request.get(auth + host + "/maxscale")
})
.then(function(resp) {
var d = JSON.parse(resp)
d.data.attributes.parameters.admin_auth.should.equal(value)
})
}
describe("Authentication", function() {
before(startMaxScale)
var user1 = {
data: {
id: "user1",
type: "inet",
attributes: {
password: "pw1"
}
}
}
var user2 = {
data: {
id: "user2",
type: "inet",
attributes: {
password: "pw2"
}
}
}
var auth1 = "http://" + user1.data.id + ":" + user1.data.attributes.password + "@"
var auth2 = "http://" + user2.data.id + ":" + user2.data.attributes.password + "@"
it("unauthorized request without authentication", function() {
return request.get(base_url + "/maxscale")
.should.be.fulfilled
})
it("authorized request without authentication", function() {
return request.get(auth1 + host + "/maxscale")
.should.be.fulfilled
})
it("add user", function() {
return request.post(base_url + "/users/inet", { json: user1 })
.should.be.fulfilled
})
it("request created user", function() {
return request.get(base_url + "/users/inet/" + user1.data.id)
.should.be.fulfilled
})
it("enable authentication", function() {
return set_auth(auth1, true).should.be.fulfilled
})
it("unauthorized request with authentication", function() {
return request.get(base_url + "/maxscale").auth()
.should.be.rejected
})
it("authorized request with authentication", function() {
return request.get(auth1 + host + "/maxscale")
.should.be.fulfilled
})
it("replace user", function() {
return request.post(auth1 + host + "/users/inet", { json: user2 })
.then(function() {
return request.get(auth1 + host + "/users/inet/" + user2.data.id)
})
.then(function() {
return request.delete(auth1 + host + "/users/inet/" + user1.data.id)
})
.should.be.fulfilled
})
it("request with wrong user", function() {
return request.get(auth1 + host + "/maxscale")
.should.be.rejected
})
it("request with correct user", function() {
return request.get(auth2 + host + "/maxscale")
.should.be.fulfilled
})
it("disable authentication", function() {
return set_auth(auth2, false).should.be.fulfilled
})
it("unauthorized request without authentication ", function() {
return request.get(base_url + "/maxscale/logs")
.should.be.fulfilled
})
it("authorized request without authentication", function() {
return request.get(auth2 + host + "/maxscale")
.should.be.fulfilled
})
after(stopMaxScale)
});

View File

@ -0,0 +1,39 @@
require("../utils.js")()
function set_value(key, value) {
return request.get(base_url + "/maxscale")
.then(function(resp) {
var d = JSON.parse(resp)
d.data.attributes.parameters[key] = value;
return request.patch(base_url + "/maxscale", { json: d })
})
.then(function() {
return request.get(base_url + "/maxscale")
})
.then(function(resp) {
var d = JSON.parse(resp)
d.data.attributes.parameters[key].should.equal(value)
})
}
describe("Core Parameters", function() {
before(startMaxScale)
it("auth_connect_timeout", function() {
return set_value("auth_connect_timeout", 10)
.should.be.fulfilled
})
it("auth_read_timeout", function() {
return set_value("auth_read_timeout", 10)
.should.be.fulfilled
})
it("auth_write_timeout", function() {
return set_value("auth_write_timeout", 10)
.should.be.fulfilled
})
after(stopMaxScale)
});

View File

@ -0,0 +1,21 @@
require("../utils.js")()
describe("Errors", function()
{
before(startMaxScale)
it("error on invalid PATCH request", function()
{
return request.patch(base_url + "/servers/server1", { json: {this_is: "a test"}})
.should.be.rejected
})
it("error on invalid POST request", function()
{
return request.post(base_url + "/servers", { json: {this_is: "a test"}})
.should.be.rejected
})
after(stopMaxScale)
});

View File

@ -0,0 +1,126 @@
require("../utils.js")()
describe("HTTP Headers", function() {
before(startMaxScale)
it("ETag changes after modification", function() {
return request.get(base_url + "/servers/server1", {resolveWithFullResponse: true})
.then(function(resp) {
resp.headers.etag.should.be.equal("\"0\"")
var srv = JSON.parse(resp.body)
delete srv.data.relationships
return request.patch(base_url + "/servers/server1", {json: srv})
})
.then(function() {
return request.get(base_url + "/servers/server1", {resolveWithFullResponse: true})
})
.then(function(resp) {
resp.headers.etag.should.be.equal("\"1\"")
})
});
it("Last-Modified changes after modification", function(done) {
var date;
request.get(base_url + "/servers/server1", {resolveWithFullResponse: true})
.then(function(resp) {
// Store the current modification time
resp.headers["last-modified"].should.not.be.null
date = resp.headers["last-modified"]
// Modify resource after three seconds
setTimeout(function() {
var srv = JSON.parse(resp.body)
srv.data.relationships = {
services: {
data: [
{id: "RW-Split-Router", type: "services"}
]
}
}
request.patch(base_url + "/servers/server1", {json: srv})
.then(function() {
return request.get(base_url + "/servers/server1", {resolveWithFullResponse: true})
})
.then(function(resp) {
resp.headers["last-modified"].should.not.be.null
resp.headers["last-modified"].should.not.be.equal(date)
done()
})
.catch(function(e) {
done(e)
})
}, 2000)
})
.catch(function(e) {
done(e)
})
});
var oldtime = new Date(new Date().getTime() - 1000000).toUTCString();
var newtime = new Date(new Date().getTime() + 1000000).toUTCString();
it("request with older If-Modified-Since value", function() {
return request.get(base_url + "/servers/server1", {
headers: { "If-Modified-Since": oldtime}
}).should.be.fulfilled
})
it("request with newer If-Modified-Since value", function() {
return request.get(base_url + "/servers/server1", {
resolveWithFullResponse: true,
headers: { "If-Modified-Since": newtime }
}).should.be.rejected
})
it("request with older If-Unmodified-Since value", function() {
return request.get(base_url + "/servers/server1", {
headers: { "If-Unmodified-Since": oldtime}
}).should.be.rejected
})
it("request with newer If-Unmodified-Since value", function() {
return request.get(base_url + "/servers/server1", {
headers: { "If-Unmodified-Since": newtime}
}).should.be.fulfilled
})
it("request with mismatching If-Match value", function() {
return request.get(base_url + "/servers/server1", {
headers: { "If-Match": "\"0\""}
}).should.be.rejected
})
it("request with matching If-Match value", function() {
return request.get(base_url + "/servers/server1", { resolveWithFullResponse: true })
.then(function(resp) {
return request.get(base_url + "/servers/server1", {
headers: { "If-Match": resp.headers["etag"]}
})
})
.should.be.fulfilled
})
it("request with mismatching If-None-Match value", function() {
return request.get(base_url + "/servers/server1", {
headers: { "If-None-Match": "\"0\""}
}).should.be.fulfilled
})
it("request with matching If-None-Match value", function() {
return request.get(base_url + "/servers/server1", { resolveWithFullResponse: true })
.then(function(resp) {
return request.get(base_url + "/servers/server1", {
headers: { "If-None-Match": resp.headers["etag"]}
})
})
.should.be.rejected
})
after(stopMaxScale)
});

View File

@ -0,0 +1,42 @@
require("../utils.js")()
describe("Logs", function() {
before(startMaxScale)
it("change logging options", function() {
return request.get(base_url + "/maxscale/logs")
.then(function(resp) {
var logs = JSON.parse(resp)
logs.data.attributes.parameters.maxlog.should.be.true
logs.data.attributes.parameters.syslog.should.be.true
logs.data.attributes.parameters.highprecision.should.be.false
logs.data.attributes.parameters.maxlog = false
logs.data.attributes.parameters.syslog = false
logs.data.attributes.parameters.highprecision = true
logs.data.attributes.parameters.throttling.count = 1
logs.data.attributes.parameters.throttling.suppress_ms = 1
logs.data.attributes.parameters.throttling.window_ms = 1
return request.patch(base_url + "/maxscale/logs", {json: logs})
})
.then(function(resp) {
return request.get(base_url + "/maxscale/logs")
})
.then(function(resp) {
var logs = JSON.parse(resp)
logs.data.attributes.parameters.maxlog.should.be.false
logs.data.attributes.parameters.syslog.should.be.false
logs.data.attributes.parameters.highprecision.should.be.true
logs.data.attributes.parameters.throttling.count.should.be.equal(1)
logs.data.attributes.parameters.throttling.suppress_ms.should.be.equal(1)
logs.data.attributes.parameters.throttling.window_ms.should.be.equal(1)
})
});
it("flush logs", function() {
return request.post(base_url + "/maxscale/logs/flush")
.should.be.fulfilled
})
after(stopMaxScale)
});

View File

@ -0,0 +1,162 @@
require("../utils.js")()
var monitor = {
data: {
id: "test-monitor",
type: "monitors",
attributes: {
module: "mysqlmon"
}
}
}
describe("Monitor", function() {
before(startMaxScale)
it("create new monitor", function() {
return request.post(base_url + "/monitors/", {json: monitor})
.should.be.fulfilled
})
it("request monitor", function() {
return request.get(base_url + "/monitors/" + monitor.data.id)
.should.be.fulfilled
});
it("alter monitor", function() {
monitor.data.attributes.parameters = {
monitor_interval: 1000
}
return request.patch(base_url + "/monitors/" + monitor.data.id, {json:monitor})
.should.be.fulfilled
});
it("destroy created monitor", function() {
return request.delete(base_url + "/monitors/" + monitor.data.id)
.should.be.fulfilled
});
after(stopMaxScale)
})
describe("Monitor Relationships", function() {
before(startMaxScale)
it("create new monitor", function() {
return request.post(base_url + "/monitors/", {json: monitor})
.should.be.fulfilled
})
it("remove relationships from old monitor", function() {
return request.get(base_url + "/monitors/MySQL-Monitor")
.then(function(resp) {
var mon = JSON.parse(resp)
delete mon.data.relationships.servers
return request.patch(base_url + "/monitors/MySQL-Monitor", {json: mon})
})
.should.be.fulfilled
});
it("add relationships to new monitor", function() {
return request.get(base_url + "/monitors/" + monitor.data.id)
.then(function(resp) {
var mon = JSON.parse(resp)
mon.data.relationships.servers = [
{id: "server1", type: "servers"},
{id: "server2", type: "servers"},
{id: "server3", type: "servers"},
{id: "server4", type: "servers"},
]
return request.patch(base_url + "/monitors/" + monitor.data.id, {json: mon})
})
.should.be.fulfilled
});
it("move relationships back to old monitor", function() {
return request.get(base_url + "/monitors/" + monitor.data.id)
.then(function(resp) {
var mon = JSON.parse(resp)
delete mon.data.relationships.servers
return request.patch(base_url + "/monitors/" + monitor.data.id, {json: mon})
})
.then(function() {
return request.get(base_url + "/monitors/MySQL-Monitor")
})
.then(function(resp) {
var mon = JSON.parse(resp)
mon.data.relationships.servers = [
{id: "server1", type: "servers"},
{id: "server2", type: "servers"},
{id: "server3", type: "servers"},
{id: "server4", type: "servers"},
]
return request.patch(base_url + "/monitors/MySQL-Monitor", {json: mon})
})
.should.be.fulfilled
});
it("destroy created monitor", function() {
return request.delete(base_url + "/monitors/" + monitor.data.id)
.should.be.fulfilled
});
after(stopMaxScale)
})
describe("Monitor Actions", function() {
before(startMaxScale)
it("stop monitor", function() {
return request.put(base_url + "/monitors/MySQL-Monitor/stop")
.then(function() {
return request.get(base_url + "/monitors/MySQL-Monitor")
})
.then(function(resp) {
var mon = JSON.parse(resp)
mon.data.attributes.state.should.be.equal("Stopped")
})
});
it("start monitor", function() {
return request.put(base_url + "/monitors/MySQL-Monitor/start")
.then(function() {
return request.get(base_url + "/monitors/MySQL-Monitor")
})
.then(function(resp) {
var mon = JSON.parse(resp)
mon.data.attributes.state.should.be.equal("Running")
})
});
after(stopMaxScale)
})
describe("Monitor Regressions", function() {
before(startMaxScale)
it("alter monitor should not remove servers", function() {
var b = {
data: {
attributes: {
parameters: {
user: "test"
}
}
}
}
return request.patch(base_url + "/monitors/MySQL-Monitor", {json: b})
.then(function() {
return request.get(base_url + "/monitors/MySQL-Monitor")
})
.then(function(resp) {
var mon = JSON.parse(resp)
mon.data.relationships.servers.data.should.not.be.empty
})
});
after(stopMaxScale)
})

View File

@ -0,0 +1,12 @@
require("../utils.js")()
describe("Request Options", function() {
before(startMaxScale)
it("pretty=true", function() {
return request.get(base_url + "/services/?pretty=true")
.should.eventually.satisfy(validate)
})
after(stopMaxScale)
});

View File

@ -0,0 +1,111 @@
// These tests use the server/test/maxscale_test.cnf configuration
require("../utils.js")()
describe("Resource Collections", function() {
before(startMaxScale)
var tests = [
"/servers",
"/sessions",
"/services",
"/monitors",
"/filters",
"/maxscale/threads",
"/maxscale/modules",
"/maxscale/tasks",
"/users",
"/users/inet",
"/users/unix",
]
tests.forEach(function(endpoint) {
it(endpoint + ': resource found', function() {
return request(base_url + endpoint)
.should.be.fulfilled
});
it(endpoint + ': resource schema is valid', function() {
return request(base_url + endpoint)
.should.eventually.satisfy(validate)
});
})
after(stopMaxScale)
});
describe("Individual Resources", function() {
before(startMaxScale)
var tests = [
"/servers/server1",
"/servers/server2",
"/services/RW-Split-Router",
"/services/RW-Split-Router/listeners",
"/monitors/MySQL-Monitor",
"/filters/Hint",
"/sessions/1",
"/maxscale/",
"/maxscale/threads/0",
"/maxscale/logs",
"/maxscale/modules/readwritesplit",
]
tests.forEach(function(endpoint) {
it(endpoint + ': resource found', function() {
return request(base_url + endpoint)
.should.be.fulfilled
});
it(endpoint + ': resource schema is valid', function() {
return request(base_url + endpoint)
.should.eventually.satisfy(validate)
});
})
after(stopMaxScale)
});
describe("Resource Self Links", function() {
before(startMaxScale)
var tests = [
"/servers",
"/sessions",
"/services",
"/monitors",
"/filters",
"/maxscale/threads",
"/maxscale/modules",
"/maxscale/tasks",
"/servers/server1",
"/servers/server2",
"/services/RW-Split-Router",
"/services/RW-Split-Router/listeners",
"/monitors/MySQL-Monitor",
"/filters/Hint",
"/sessions/1",
"/maxscale/",
"/maxscale/threads/0",
"/maxscale/logs",
"/maxscale/modules/readwritesplit",
]
tests.forEach(function(endpoint) {
it(endpoint + ': correct self link', function() {
var obj = null;
return request.get(base_url + endpoint)
.then(function(resp) {
obj = JSON.parse(resp)
return request.get(obj.links.self)
})
.then(function(resp) {
var obj_self = JSON.parse(resp)
obj_self.links.self.should.be.equal(obj.links.self)
})
.should.be.fulfilled
});
})
after(stopMaxScale)
});

View File

@ -0,0 +1,131 @@
require("../utils.js")()
var server = {
data: {
id: "test-server",
type: "servers",
attributes: {
parameters: {
port: 3003,
address: "127.0.0.1",
protocol: "MySQLBackend"
}
}
}
};
var rel = {
services: {
data: [
{ id: "RW-Split-Router", type: "services" },
{ id: "Read-Connection-Router", type: "services" },
]
}
};
describe("Server", function() {
before(startMaxScale)
it("create new server", function() {
return request.post(base_url + "/servers/", {json: server })
.should.be.fulfilled
});
it("request server", function() {
return request.get(base_url + "/servers/" + server.data.id)
.should.be.fulfilled
});
it("update server", function() {
server.data.attributes.parameters.weight = 10
return request.patch(base_url + "/servers/" + server.data.id, { json: server})
.should.be.fulfilled
});
it("destroy server", function() {
return request.delete(base_url + "/servers/" + server.data.id)
.should.be.fulfilled
});
after(stopMaxScale)
});
describe("Server Relationships", function() {
before(startMaxScale)
// We need a deep copy of the original server
var rel_server = JSON.parse(JSON.stringify(server))
rel_server.data.relationships = rel
it("create new server", function() {
return request.post(base_url + "/servers/", {json: rel_server})
.should.be.fulfilled
});
it("request server", function() {
return request.get(base_url + "/servers/" + rel_server.data.id)
.should.be.fulfilled
});
it("remove relationships", function() {
delete rel_server.data.relationships["services"]
delete rel_server.data.relationships["monitors"]
return request.patch(base_url + "/servers/" + rel_server.data.id, {json: rel_server})
.should.be.fulfilled
});
it("destroy server", function() {
return request.delete(base_url + "/servers/" + rel_server.data.id)
.should.be.fulfilled
});
after(stopMaxScale)
});
describe("Server State", function() {
before(startMaxScale)
it("create new server", function() {
return request.post(base_url + "/servers/", {json: server })
.should.be.fulfilled
});
it("set server into maintenance", function() {
return request.put(base_url + "/servers/" + server.data.id + "/set?state=maintenance")
.then(function(resp) {
return request.get(base_url + "/servers/" + server.data.id)
})
.then(function(resp) {
var srv = JSON.parse(resp)
srv.data.attributes.state.should.match(/Maintenance/)
})
});
it("clear maintenance", function() {
return request.put(base_url + "/servers/" + server.data.id + "/clear?state=maintenance")
.then(function(resp) {
return request.get(base_url + "/servers/" + server.data.id)
})
.then(function(resp) {
var srv = JSON.parse(resp)
srv.data.attributes.state.should.not.match(/Maintenance/)
})
});
it("set invalid state value", function() {
return request.put(base_url + "/servers/" + server.data.id + "/set?state=somethingstrange")
.should.be.rejected
});
it("clear invalid state value", function() {
return request.put(base_url + "/servers/" + server.data.id + "/clear?state=somethingstrange")
.should.be.rejected
});
it("destroy server", function() {
return request.delete(base_url + "/servers/" + server.data.id)
.should.be.fulfilled
});
after(stopMaxScale)
});

View File

@ -0,0 +1,90 @@
require("../utils.js")()
describe("Service", function() {
before(startMaxScale)
it("change service parameter", function() {
return request.get(base_url + "/services/RW-Split-Router")
.then(function(resp) {
var svc = JSON.parse(resp)
svc.data.attributes.parameters.enable_root_user = true
return request.patch(base_url + "/services/RW-Split-Router", {json: svc})
})
.then(function(resp) {
return request.get(base_url + "/services/RW-Split-Router")
})
.then(function(resp) {
var svc = JSON.parse(resp)
svc.data.attributes.parameters.enable_root_user.should.be.true
})
});
it("remove service relationship", function() {
return request.get(base_url + "/services/RW-Split-Router")
.then(function(resp) {
var svc = JSON.parse(resp)
delete svc.data.relationships["servers"]
delete svc.data.relationships["servers"]
return request.patch(base_url + "/services/RW-Split-Router", {json: svc})
})
.then(function(resp) {
return request.get(base_url + "/services/RW-Split-Router")
})
.then(function(resp) {
var svc = JSON.parse(resp)
svc.data.relationships.should.be.empty
})
});
it("add service relationship", function() {
return request.get(base_url + "/services/RW-Split-Router")
.then(function(resp) {
var svc = JSON.parse(resp)
svc.data.relationships = {
servers: {
data: [
{id: "server1", type: "servers"},
{id: "server2", type: "servers"},
{id: "server3", type: "servers"},
{id: "server4", type: "servers"},
]
}
}
return request.patch(base_url + "/services/RW-Split-Router", {json: svc})
})
.then(function(resp) {
return request.get(base_url + "/services/RW-Split-Router")
})
.then(function(resp) {
var svc = JSON.parse(resp)
svc.data.relationships.servers.data[0].id.should.be.equal("server1")
})
});
it("create a listener", function() {
var listener = {
"links": {
"self": "http://localhost:8989/v1/services/RW-Split-Router/listeners"
},
"data": {
"attributes": {
"parameters": {
"port": 4012,
"protocol": "MySQLClient",
"authenticator": "MySQLAuth",
"address": "127.0.0.1"
}
},
"id": "RW-Split-Listener-2",
"type": "listeners"
}
}
return request.post(base_url + "/services/RW-Split-Router/listeners", {json: listener})
.should.be.fulfilled
});
after(stopMaxScale)
});

View File

@ -0,0 +1,59 @@
require("../utils.js")()
describe("Users", function() {
before(startMaxScale)
var user = {
data: {
id: "user1",
type: "inet",
attributes: {
}
}
}
it("add new user without password", function() {
return request.post(base_url + "/users/inet", { json: user })
.should.be.rejected
})
it("add user", function() {
user.data.attributes.password = "pw1"
return request.post(base_url + "/users/inet", { json: user })
.should.be.fulfilled
})
it("add user again", function() {
return request.post(base_url + "/users/inet", { json: user })
.should.be.rejected
})
it("add user again but without password", function() {
delete user.data.attributes.password
return request.post(base_url + "/users/inet", { json: user })
.should.be.rejected
})
it("get created user", function() {
return request.get(base_url + "/users/inet/user1")
.should.be.fulfilled
})
it("get non-existent user", function() {
return request.get(base_url + "/users/inet/user2")
.should.be.rejected
})
it("delete created user", function() {
return request.delete(base_url + "/users/inet/user1")
.should.be.fulfilled
})
it("delete created user again", function() {
return request.delete(base_url + "/users/inet/user1")
.should.be.rejected
})
after(stopMaxScale)
});

View File

@ -0,0 +1,83 @@
#!/bin/bash
# This script builds and installs MaxScale, starts a MaxScale instance, runs the
# tests use npm and stops MaxScale.
#
# This is definitely not the most efficient way to test the binaries but it's a
# guaranteed method of creating a consistent and "safe" testing environment.
#
# TODO: Install and start a local MariaDB server for testing purposes
srcdir=$1
maxscaledir=$PWD/maxscale_test/
testdir=$PWD/local_test/
mkdir -p $testdir && cd $testdir
# Currently all tests that use npm are for the REST API
cp -t $testdir -r $srcdir/server/core/test/rest-api/*
# Bring MariaDB servers up, this is an asynchronous process
docker-compose up -d || exit 1
# Install dependencies
npm install
mkdir -p $maxscaledir && cd $maxscaledir
# Configure and install MaxScale
cmake $srcdir -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_INSTALL_PREFIX=$maxscaledir \
-DBUILD_TESTS=Y \
-DMAXSCALE_VARDIR=$maxscaledir \
-DCMAKE_BUILD_TYPE=Debug \
-DWITH_SCRIPTS=N \
-DWITH_MAXSCALE_CNF=N \
-DBUILD_CDC=Y \
-DTARGET_COMPONENT=all \
-DDEFAULT_MODULE_CONFIGDIR=$maxscaledir \
-DDEFAULT_ADMIN_USER=`whoami`
make install
# Create required directories (we could run the postinst script but it's a bit too invasive)
mkdir -p $maxscaledir/lib64/maxscale
mkdir -p $maxscaledir/bin
mkdir -p $maxscaledir/share/maxscale
mkdir -p $maxscaledir/share/doc/MaxScale/maxscale
mkdir -p $maxscaledir/log/maxscale
mkdir -p $maxscaledir/lib/maxscale
mkdir -p $maxscaledir/cache/maxscale
mkdir -p $maxscaledir/run/maxscale
chmod 0755 $maxscaledir/log/maxscale
chmod 0755 $maxscaledir/lib/maxscale
chmod 0755 $maxscaledir/cache/maxscale
chmod 0755 $maxscaledir/run/maxscale
# Go to the test directory
cd $testdir
# This variable is used to start and stop MaxScale before each test
export MAXSCALE_DIR=$maxscaledir
# Wait until the servers are up
for node in server1 server2 server3 server4
do
printf "Waiting for $node to start... "
for ((i=0; i<60; i++))
do
docker-compose exec $node mysql -umaxuser -pmaxpwd -e "select 1" >& /dev/null && break
sleep 1
done
echo "Done!"
done
# Run tests
npm test
rval=$?
# Stop MariaDB servers
docker-compose down -v
exit $rval

View File

@ -0,0 +1,439 @@
var json_api_schema = {
"title": "JSON API Schema",
"description": "This is a schema for responses in the JSON API format. For more, see http://jsonapi.org",
"oneOf": [
{
"$ref": "#/definitions/success"
},
{
"$ref": "#/definitions/failure"
},
{
"$ref": "#/definitions/info"
}
],
"definitions": {
"success": {
"type": "object",
"required": [
"data"
],
"properties": {
"data": {
"$ref": "#/definitions/data"
},
"included": {
"description": "To reduce the number of HTTP requests, servers **MAY** allow responses that include related resources along with the requested primary resources. Such responses are called \"compound documents\".",
"type": "array",
"items": {
"$ref": "#/definitions/resource"
},
"uniqueItems": true
},
"meta": {
"$ref": "#/definitions/meta"
},
"links": {
"description": "Link members related to the primary data.",
"allOf": [
{
"$ref": "#/definitions/links"
},
{
"$ref": "#/definitions/pagination"
}
]
},
"jsonapi": {
"$ref": "#/definitions/jsonapi"
}
},
"additionalProperties": false
},
"failure": {
"type": "object",
"required": [
"errors"
],
"properties": {
"errors": {
"type": "array",
"items": {
"$ref": "#/definitions/error"
},
"uniqueItems": true
},
"meta": {
"$ref": "#/definitions/meta"
},
"jsonapi": {
"$ref": "#/definitions/jsonapi"
},
"links": {
"$ref": "#/definitions/links"
}
},
"additionalProperties": false
},
"info": {
"type": "object",
"required": [
"meta"
],
"properties": {
"meta": {
"$ref": "#/definitions/meta"
},
"links": {
"$ref": "#/definitions/links"
},
"jsonapi": {
"$ref": "#/definitions/jsonapi"
}
},
"additionalProperties": false
},
"meta": {
"description": "Non-standard meta-information that can not be represented as an attribute or relationship.",
"type": "object",
"additionalProperties": true
},
"data": {
"description": "The document's \"primary data\" is a representation of the resource or collection of resources targeted by a request.",
"oneOf": [
{
"$ref": "#/definitions/resource"
},
{
"description": "An array of resource objects, an array of resource identifier objects, or an empty array ([]), for requests that target resource collections.",
"type": "array",
"items": {
"$ref": "#/definitions/resource"
},
"uniqueItems": true
},
{
"description": "null if the request is one that might correspond to a single resource, but doesn't currently.",
"type": "null"
}
]
},
"resource": {
"description": "\"Resource objects\" appear in a JSON API document to represent resources.",
"type": "object",
"required": [
"type",
"id"
],
"properties": {
"type": {
"type": "string"
},
"id": {
"type": "string"
},
"attributes": {
"$ref": "#/definitions/attributes"
},
"relationships": {
"$ref": "#/definitions/relationships"
},
"links": {
"$ref": "#/definitions/links"
},
"meta": {
"$ref": "#/definitions/meta"
}
},
"additionalProperties": false
},
"links": {
"description": "A resource object **MAY** contain references to other resource objects (\"relationships\"). Relationships may be to-one or to-many. Relationships can be specified by including a member in a resource's links object.",
"type": "object",
"properties": {
"self": {
"description": "A `self` member, whose value is a URL for the relationship itself (a \"relationship URL\"). This URL allows the client to directly manipulate the relationship. For example, it would allow a client to remove an `author` from an `article` without deleting the people resource itself.",
"type": "string",
"format": "uri"
},
"related": {
"$ref": "#/definitions/link"
}
},
"additionalProperties": true
},
"link": {
"description": "A link **MUST** be represented as either: a string containing the link's URL or a link object.",
"oneOf": [
{
"description": "A string containing the link's URL.",
"type": "string",
"format": "uri"
},
{
"type": "object",
"required": [
"href"
],
"properties": {
"href": {
"description": "A string containing the link's URL.",
"type": "string",
"format": "uri"
},
"meta": {
"$ref": "#/definitions/meta"
}
}
}
]
},
"attributes": {
"description": "Members of the attributes object (\"attributes\") represent information about the resource object in which it's defined.",
"type": "object",
"patternProperties": {
"^(?!relationships$|links$)\\w[-\\w_]*$": {
"description": "Attributes may contain any valid JSON value."
}
},
"additionalProperties": false
},
"relationships": {
"description": "Members of the relationships object (\"relationships\") represent references from the resource object in which it's defined to other resource objects.",
"type": "object",
"patternProperties": {
"^\\w[-\\w_]*$": {
"properties": {
"links": {
"$ref": "#/definitions/links"
},
"data": {
"description": "Member, whose value represents \"resource linkage\".",
"oneOf": [
{
"$ref": "#/definitions/relationshipToOne"
},
{
"$ref": "#/definitions/relationshipToMany"
}
]
},
"meta": {
"$ref": "#/definitions/meta"
}
},
"anyOf": [
{
"required": [
"data"
]
},
{
"required": [
"meta"
]
},
{
"required": [
"links"
]
}
],
"additionalProperties": false
}
},
"additionalProperties": false
},
"relationshipToOne": {
"description": "References to other resource objects in a to-one (\"relationship\"). Relationships can be specified by including a member in a resource's links object.",
"anyOf": [
{
"$ref": "#/definitions/empty"
},
{
"$ref": "#/definitions/linkage"
}
]
},
"relationshipToMany": {
"description": "An array of objects each containing \"type\" and \"id\" members for to-many relationships.",
"type": "array",
"items": {
"$ref": "#/definitions/linkage"
},
"uniqueItems": true
},
"empty": {
"description": "Describes an empty to-one relationship.",
"type": "null"
},
"linkage": {
"description": "The \"type\" and \"id\" to non-empty members.",
"type": "object",
"required": [
"type",
"id"
],
"properties": {
"type": {
"type": "string"
},
"id": {
"type": "string"
},
"meta": {
"$ref": "#/definitions/meta"
}
},
"additionalProperties": false
},
"pagination": {
"type": "object",
"properties": {
"first": {
"description": "The first page of data",
"oneOf": [
{
"type": "string",
"format": "uri"
},
{
"type": "null"
}
]
},
"last": {
"description": "The last page of data",
"oneOf": [
{
"type": "string",
"format": "uri"
},
{
"type": "null"
}
]
},
"prev": {
"description": "The previous page of data",
"oneOf": [
{
"type": "string",
"format": "uri"
},
{
"type": "null"
}
]
},
"next": {
"description": "The next page of data",
"oneOf": [
{
"type": "string",
"format": "uri"
},
{
"type": "null"
}
]
}
}
},
"jsonapi": {
"description": "An object describing the server's implementation",
"type": "object",
"properties": {
"version": {
"type": "string"
},
"meta": {
"$ref": "#/definitions/meta"
}
},
"additionalProperties": false
},
"error": {
"type": "object",
"properties": {
"id": {
"description": "A unique identifier for this particular occurrence of the problem.",
"type": "string"
},
"links": {
"$ref": "#/definitions/links"
},
"status": {
"description": "The HTTP status code applicable to this problem, expressed as a string value.",
"type": "string"
},
"code": {
"description": "An application-specific error code, expressed as a string value.",
"type": "string"
},
"title": {
"description": "A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization.",
"type": "string"
},
"detail": {
"description": "A human-readable explanation specific to this occurrence of the problem.",
"type": "string"
},
"source": {
"type": "object",
"properties": {
"pointer": {
"description": "A JSON Pointer [RFC6901] to the associated entity in the request document [e.g. \"/data\" for a primary data object, or \"/data/attributes/title\" for a specific attribute].",
"type": "string"
},
"parameter": {
"description": "A string indicating which query parameter caused the error.",
"type": "string"
}
}
},
"meta": {
"$ref": "#/definitions/meta"
}
},
"additionalProperties": false
}
}
}
function validate_json(data) {
return validate_func(JSON.parse(data))
}
var child_process = require("child_process")
module.exports = function() {
this.fs = require("fs")
this.request = require("request-promise-native")
this.chai = require("chai")
this.assert = require("assert")
this.chaiAsPromised = require("chai-as-promised")
chai.use(chaiAsPromised)
this.should = chai.should()
this.expect = chai.expect
this.Ajv = require("ajv")
this.ajv = new Ajv({$data: true, allErrors: true, extendRefs: true, verbose: true})
this.validate_func = ajv.compile(json_api_schema)
this.validate = validate_json
this.host = "localhost:8989/v1"
this.base_url = "http://" + this.host
this.startMaxScale = function(done) {
child_process.execFile("./before.sh", function(err, stdout, stderr) {
if (process.env.MAXSCALE_DIR == null) {
throw new Error("MAXSCALE_DIR is not set");
}
done()
})
};
this.stopMaxScale = function(done) {
child_process.execFile("./after.sh", function(err, stdout, stderr) {
done()
})
};
}