Skip to content
This repository has been archived by the owner on May 24, 2020. It is now read-only.

Commit

Permalink
Use a global setup which actually blocks correctly
Browse files Browse the repository at this point in the history
This doesn't work yet, pending jestjs/jest#9622 (Jest 26)
  • Loading branch information
kegsay committed Apr 26, 2020
1 parent 5d2a924 commit 81aa6bd
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 61 deletions.
3 changes: 1 addition & 2 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,5 @@ module.exports = {
"**/src/tests/**/*.ts"
],
moduleFileExtensions: ["ts", "tsx", "js"],
globalSetup: "<rootDir>/src/globalSetup.ts",
globalTeardown: "<rootDir>/src/globalTeardown.ts"
testEnvironment: "<rootDir>/src/sytsEnvironment.ts",
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"axios": "^0.19.2",
"dockerode": "^3.1.0",
"jest": "^25.1.0",
"jest-environment-node": "^25.4.0",
"js-yaml": "^3.13.1",
"json-schema-to-typescript": "^8.1.0",
"ts-jest": "^25.2.0",
Expand Down
15 changes: 11 additions & 4 deletions src/blueprints/loader/blueprint-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,17 @@ function normaliseLocalpartToCompleteUserID(
function resolveUserIds(blueprint: Blueprint) {
blueprint.homeservers?.forEach((hs) => {
hs.users?.forEach((user) => {
user.localpart = normaliseLocalpartToCompleteUserID(
user.localpart,
hs.name
);
if (user.localpart[0] !== "@") {
throw new Error(
`HS ${hs.name} User ${user.localpart} must start with '@'`
);
}
if (user.localpart.includes(":")) {
throw new Error(
`HS ${hs.name} User ${user.localpart} must not contain a domain`
);
}
user.localpart = user.localpart.replace("@", "");
});
hs.rooms?.forEach((room) => {
room.creator = normaliseLocalpartToCompleteUserID(
Expand Down
92 changes: 57 additions & 35 deletions src/blueprints/loader/instructions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Homeserver } from "./blueprint";
import axios, { Method } from "axios";
import axios, { Method, AxiosResponse } from "axios";

// Instruction represents an HTTP request which should be made to a remote server
type Instruction = {
Expand All @@ -13,8 +13,9 @@ type Instruction = {
// The access_token to use in the request, represented as a key to use in the lookup table e.g "user_@alice:localhost"
// Null if no token should be used (e.g /register requests).
access_token: string | null;
// The path or query placeholders to replace e.g "/foo/$roomId" with the substitution { $roomId: "room_1"}.
// The key is the path param e.g $foo and the value is the lookup table key e.g "room_id".
// The path or query placeholders to replace e.g "/foo/$roomId" with the substitution { $roomId: ".room_1"}.
// The key is the path param e.g $foo and the value is the lookup table key e.g ".room_id". If the value does not
// start with a '.' it is interpreted as a literal string to be substituted. e.g { $eventType: "m.room.message" }
substitutions?: Record<string, string>;
// The fields (expressed as dot-style notation) which should be stored in a lookup table for later use.
// E.g to store the room_id in the response under the key 'foo' to use it later: { "foo" : ".room_id" }
Expand All @@ -34,44 +35,47 @@ export class InstructionRunner {
instructions: Array<Instruction>;
lookup: { [key: string]: string };
index: number;
contextStr: string;

constructor(hs: Homeserver) {
constructor(hs: Homeserver, contextStr: string) {
this.instructions = calculateInstructions(hs);
this.lookup = {};
this.index = 0;
this.contextStr = contextStr;
}

// Run all instructions until completion. Throws if there was a problem executing any instruction.
async run(baseUrl: string) {
let entry = this.next(baseUrl);
while (entry != undefined) {
let response: AxiosResponse;
try {
const response = await axios.request(entry.req);
if (response.status < 200 || response.status >= 300) {
throw new Error(
`Request ${JSON.stringify(entry.req)} returned HTTP ${
response.status
} : ${response.data}`
);
}

if (entry.instr.storeResponse) {
for (let [key, value] of Object.entries(
entry.instr.storeResponse
)) {
this.lookup[key] = mapDotStyleKey(response.data, value);
}
}
response = await axios.request(entry.req);
} catch (err) {
console.error(
`Error: ${JSON.stringify(entry.req)} returned =====> HTTP ${
err.response.status
} => ${JSON.stringify(err.response.data)}`
);
response = err.response;
}
console.log(
`${this.contextStr} [${entry.instr.access_token}] ${entry.req.url} => HTTP ${response.status}`
);
if (response.status < 200 || response.status >= 300) {
console.log("LOOKUP : " + JSON.stringify(this.lookup));
console.log("INSTRUCTION: " + JSON.stringify(entry.instr));
throw new Error(
`Failed to execute HTTP requests on ${baseUrl}`
`${this.contextStr} Request ${JSON.stringify(
entry.req
)} returned HTTP ${response.status} : ${JSON.stringify(
response.data
)}`
);
}

if (entry.instr.storeResponse) {
for (let [key, value] of Object.entries(
entry.instr.storeResponse
)) {
this.lookup[key] = mapDotStyleKey(response.data, value);
}
}
entry = this.next(baseUrl);
}
}
Expand Down Expand Up @@ -113,7 +117,7 @@ function calculateInstructions(hs: Homeserver): Array<Instruction> {
// add instructions to create users
hs.users?.forEach((user) => {
const storeRes: { [key: string]: string } = {};
storeRes[`user_${user.localpart}`] = ".access_token";
storeRes[`user_@${user.localpart}:${hs.name}`] = ".access_token";

instructions.push({
method: "POST",
Expand Down Expand Up @@ -143,20 +147,32 @@ function calculateInstructions(hs: Homeserver): Array<Instruction> {
});
room.events?.forEach((event, eventIndex) => {
let path = "";
let method: Method = "PUT";
const subs: { [key: string]: string } = {};
subs["roomId"] = `room_${roomIndex}`;
subs["eventType"] = event.type;
subs["$roomId"] = `.room_${roomIndex}`;
subs["$eventType"] = event.type;
if (event.state_key != null) {
path =
"/_matrix/client/r0/rooms/{roomId}/state/{eventType}/{stateKey}";
subs["stateKey"] = `${event.state_key}`;
"/_matrix/client/r0/rooms/$roomId/state/$eventType/$stateKey";
subs["$stateKey"] = `${event.state_key}`;
} else {
path =
"/_matrix/client/r0/rooms/{roomId}/send/{eventType}/{txnId}";
subs["txnId"] = `${eventIndex}`;
"/_matrix/client/r0/rooms/$roomId/send/$eventType/$txnId";
subs["$txnId"] = `${eventIndex}`;
}

// special cases: room joining
if (
event.type === "m.room.member" &&
event.content &&
event.content.membership === "join"
) {
path = "/_matrix/client/r0/join/$roomId";
method = "POST";
}

instructions.push({
method: "PUT",
method: method,
path: path,
body: JSON.stringify(room.createRoom),
access_token: `user_${event.sender}`,
Expand Down Expand Up @@ -187,9 +203,15 @@ function encodeUri(
if (!variables.hasOwnProperty(key)) {
continue;
}
const lookupKey = variables[key];
// if the variable start with a '.' then use the lookup table, else use the string literally
// this handles scenarios like:
// { $roomId: ".room_0", $eventType: "m.room.message" }
const valToEncode =
lookupKey[0] === "." ? lookup[lookupKey.substr(1)] : lookupKey;
pathTemplate = pathTemplate.replace(
key,
encodeURIComponent(lookup[variables[key]])
encodeURIComponent(valToEncode)
);
}
return pathTemplate;
Expand Down
2 changes: 1 addition & 1 deletion src/blueprints/one_to_one_room.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ homeservers: [
rooms: [
{
createRoom: {
preset: "private_chat",
preset: "public_chat",
invite: ["@bob:hs1"]
},
creator: "@alice",
Expand Down
3 changes: 1 addition & 2 deletions src/builder/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class DockerBuilder {
const docker = new Docker({ socketPath: this.dockerSock });
const server = await this._deployBaseImage(docker, contextStr);
try {
const runner = new InstructionRunner(hs);
const runner = new InstructionRunner(hs, contextStr);
await runner.run(server.baseUrl);

const imageId = await this._commitImage(
Expand Down Expand Up @@ -164,7 +164,6 @@ class DockerBuilder {
repo: "syts",
tag: contextStr,
});
console.log(r);
const imageId: string = r["Id"];
return imageId.replace("sha256:", "");
}
Expand Down
9 changes: 0 additions & 9 deletions src/globalTeardown.ts

This file was deleted.

47 changes: 39 additions & 8 deletions src/globalSetup.ts → src/sytsEnvironment.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
// This file gets executed as a globalSetup hook when Jest runs. See jest.config.js
import NodeEnvironment from "jest-environment-node";
import DockerBuilder from "./builder/docker";
import loadBlueprints from "./blueprints/loader/blueprint-loader";

/*
globalSetup +---------------------+
This is the main entry point for SyTS. This file governs:
- Loading blueprints.
- Creating homeserver base containers.
- Running blueprints on containers.
- Committing the containers as new images with well-defined names $blueprintName:$hsName
Tests will then ask for a deployment of a blueprint by name which will deploy potentially
multiple servers (if testing Federation). Those servers can then be poked until the deployment
is destroyed.
setup (this file) +---------------------+
| Docker |
+------------+ +---------+ runs | +--------+ |
| Blueprints | -------> | Builder | -----------> | | Images | |
Expand All @@ -18,12 +31,7 @@ test process | |
*/

import DockerBuilder from "./builder/docker";
import loadBlueprints from "./blueprints/loader/blueprint-loader";

// TODO: Buiild all blueprints and commit images for them.

module.exports = async function () {
const setupDocker = async function () {
console.log("Loading and building all blueprints...");
const blueprints = loadBlueprints();
const builder = new DockerBuilder(
Expand All @@ -33,3 +41,26 @@ module.exports = async function () {
);
await builder.constructBlueprints(blueprints);
};

const teardownDocker = async function () {
// TODO delete running containers
// TODO delete created images
};

class SytsEnvironment extends NodeEnvironment {
constructor(config: any) {
super(config);
}

async setup() {
await super.setup();
await setupDocker();
}

async teardown() {
await super.teardown();
await teardownDocker();
}
}

export default SytsEnvironment;
Loading

0 comments on commit 81aa6bd

Please sign in to comment.