improve test setup and code style

This commit is contained in:
Franz Heinzmann (Frando)
2022-06-29 22:24:18 +02:00
committed by Simon Laux
parent bdd4aa0f10
commit 2f00b098ac
9 changed files with 196 additions and 212 deletions

View File

@@ -3,11 +3,11 @@ name = "deltachat-jsonrpc"
version = "1.86.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021"
default-run = "webserver"
default-run = "deltachat-jsonrpc-server"
license = "MPL-2.0"
[[bin]]
name = "webserver"
name = "deltachat-jsonrpc-server"
path = "src/webserver.rs"
required-features = ["webserver"]
@@ -36,6 +36,3 @@ tokio = { version = "1.19.2", features = ["full", "rt-multi-thread"] }
[features]
default = []
webserver = ["env_logger", "axum", "tokio/full", "yerpc/support-axum"]
[profile.release]
lto = true

View File

@@ -9,14 +9,19 @@
"scripts": {
"prettier:check": "prettier --check **.ts",
"prettier:fix": "prettier --write **.ts",
"build": "npm run generate-bindings && tsc",
"bundle": "npm run build && esbuild --bundle dist/deltachat.js --outfile=dist/deltachat.bundle.js",
"generate-bindings": "cargo test",
"example:build": "tsc && esbuild --bundle dist/example/example.js --outfile=dist/example.bundle.js",
"build": "run-s generate-bindings build:tsc build:bundle",
"build:tsc": "tsc",
"build:bundle": "esbuild --bundle dist/deltachat.js --outfile=dist/deltachat.bundle.js",
"example": "run-s build example:build example:start",
"example:build": "esbuild --bundle dist/example/example.js --outfile=dist/example.bundle.js",
"example:start": "http-server .",
"example:dev": "esbuild example/example.ts --bundle --outfile=dist/example.bundle.js --servedir=.",
"coverage": "tsc -b test && COVERAGE=1 NODE_OPTIONS=--enable-source-maps c8 --include \"dist/*\" -r text -r html -r json mocha test_dist && node report_api_coverage.mjs",
"test": "rm -rf dist && npm run build && npm run coverage && npm run prettier:check",
"test": "run-s test:prepare test:run-coverage test:report-coverage",
"test:prepare": "cargo build --features webserver --bin deltachat-jsonrpc-server",
"test:run": "mocha dist/test",
"test:run-coverage": "COVERAGE=1 NODE_OPTIONS=--enable-source-maps c8 --include 'dist/*' -r text -r html -r json mocha dist/test",
"test:report-coverage": "node report_api_coverage.mjs",
"docs": "typedoc --out docs deltachat.ts"
},
"dependencies": {
@@ -34,8 +39,10 @@
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"esbuild": "^0.14.11",
"http-server": "^14.1.1",
"mocha": "^9.1.1",
"node-fetch": "^2.6.1",
"npm-run-all": "^4.1.5",
"prettier": "^2.6.2",
"typedoc": "^0.23.2",
"typescript": "^4.5.5",

View File

@@ -1,14 +1,14 @@
import { readFileSync } from "fs";
// only checks for the coverge of the api functions in bindings.ts for now
const generated_file = "typescript/generated/client.ts";
const generatedFile = "typescript/generated/client.ts";
const json = JSON.parse(readFileSync("./coverage/coverage-final.json"));
const jsonCoverage =
json[Object.keys(json).find((k) => k.includes(generated_file))];
json[Object.keys(json).find((k) => k.includes(generatedFile))];
const fnMap = Object.keys(jsonCoverage.fnMap).map(
(key) => jsonCoverage.fnMap[key]
);
const htmlCoverage = readFileSync(
"./coverage/" + generated_file + ".html",
"./coverage/" + generatedFile + ".html",
"utf8"
);
const uncoveredLines = htmlCoverage
@@ -22,7 +22,7 @@ console.log(
uncoveredFunctions
.map((uF) => fnMap.find(({ name }) => name === uF))
.map(
({ name, line }) => `.${name.padEnd(40)} (${generated_file}:${line})`
({ name, line }) => `.${name.padEnd(40)} (${generatedFile}:${line})`
)
.join("\n")
);

View File

@@ -2,59 +2,55 @@ import { strictEqual } from "assert";
import chai, { assert, expect } from "chai";
import chaiAsPromised from "chai-as-promised";
chai.use(chaiAsPromised);
import { Deltachat } from "../dist/deltachat.js";
import { Deltachat } from "../deltachat.js";
import {
CMD_API_Server_Handle,
CMD_API_SERVER_PORT,
startCMD_API_Server,
RpcServerHandle,
startServer,
} from "./test_base.js";
describe("basic tests", () => {
let server_handle: CMD_API_Server_Handle;
let serverHandle: RpcServerHandle;
let dc: Deltachat;
before(async () => {
server_handle = await startCMD_API_Server(CMD_API_SERVER_PORT);
serverHandle = await startServer();
// make sure server is up by the time we continue
await new Promise((res) => setTimeout(res, 100));
dc = new Deltachat({
url: "ws://localhost:" + CMD_API_SERVER_PORT + "/ws",
});
dc.on("ALL", (event) => {
dc = new Deltachat(serverHandle.url)
// dc.on("ALL", (event) => {
//console.log("event", event);
});
// });
});
after(async () => {
dc && dc.close();
await server_handle.close();
await serverHandle.close();
});
it("check email", async () => {
const positive_test_cases = [
it("check email address validity", async () => {
const validAddresses = [
"email@example.com",
"36aa165ae3406424e0c61af17700f397cad3fe8ab83d682d0bddf3338a5dd52e@yggmail@yggmail",
];
const negative_test_cases = ["email@", "example.com", "emai221"];
const invalidAddresses = ["email@", "example.com", "emai221"];
expect(
await Promise.all(
positive_test_cases.map((email) => dc.rpc.checkEmailValidity(email))
validAddresses.map((email) => dc.rpc.checkEmailValidity(email))
)
).to.not.contain(false);
expect(
await Promise.all(
negative_test_cases.map((email) => dc.rpc.checkEmailValidity(email))
invalidAddresses.map((email) => dc.rpc.checkEmailValidity(email))
)
).to.not.contain(true);
});
it("system info", async () => {
const system_info = await dc.rpc.getSystemInfo();
expect(system_info).to.contain.keys([
const systemInfo = await dc.rpc.getSystemInfo();
expect(systemInfo).to.contain.keys([
"arch",
"num_cpus",
"deltachat_core_version",
@@ -64,7 +60,8 @@ describe("basic tests", () => {
describe("account managment", () => {
it("should create account", async () => {
await dc.rpc.addAccount();
const res = await dc.rpc.addAccount();
console.log('res', res)
assert((await dc.rpc.getAllAccountIds()).length === 1);
});
@@ -83,55 +80,55 @@ describe("basic tests", () => {
});
describe("contact managment", function () {
let acc: number;
let accountId: number;
before(async () => {
acc = await dc.rpc.addAccount();
accountId = await dc.rpc.addAccount();
});
it("block and unblock contact", async function () {
it("should block and unblock contact", async function () {
const contactId = await dc.rpc.contactsCreateContact(
acc,
accountId,
"example@delta.chat",
null
);
expect((await dc.rpc.contactsGetContact(acc, contactId)).is_blocked).to.be
expect((await dc.rpc.contactsGetContact(accountId, contactId)).is_blocked).to.be
.false;
await dc.rpc.contactsBlock(acc, contactId);
expect((await dc.rpc.contactsGetContact(acc, contactId)).is_blocked).to.be
await dc.rpc.contactsBlock(accountId, contactId);
expect((await dc.rpc.contactsGetContact(accountId, contactId)).is_blocked).to.be
.true;
expect(await dc.rpc.contactsGetBlocked(acc)).to.have.length(1);
await dc.rpc.contactsUnblock(acc, contactId);
expect((await dc.rpc.contactsGetContact(acc, contactId)).is_blocked).to.be
expect(await dc.rpc.contactsGetBlocked(accountId)).to.have.length(1);
await dc.rpc.contactsUnblock(accountId, contactId);
expect((await dc.rpc.contactsGetContact(accountId, contactId)).is_blocked).to.be
.false;
expect(await dc.rpc.contactsGetBlocked(acc)).to.have.length(0);
expect(await dc.rpc.contactsGetBlocked(accountId)).to.have.length(0);
});
});
describe("configuration", function () {
let acc: number;
let accountId: number;
before(async () => {
acc = await dc.rpc.addAccount();
accountId = await dc.rpc.addAccount();
});
it("set and retrive", async function () {
await dc.rpc.setConfig(acc, "addr", "valid@email");
assert((await dc.rpc.getConfig(acc, "addr")) == "valid@email");
await dc.rpc.setConfig(accountId, "addr", "valid@email");
assert((await dc.rpc.getConfig(accountId, "addr")) == "valid@email");
});
it("set invalid key should throw", async function () {
await expect(dc.rpc.setConfig(acc, "invalid_key", "some value")).to.be
await expect(dc.rpc.setConfig(accountId, "invalid_key", "some value")).to.be
.eventually.rejected;
});
it("get invalid key should throw", async function () {
await expect(dc.rpc.getConfig(acc, "invalid_key")).to.be.eventually
await expect(dc.rpc.getConfig(accountId, "invalid_key")).to.be.eventually
.rejected;
});
it("set and retrive ui.*", async function () {
await dc.rpc.setConfig(acc, "ui.chat_bg", "color:red");
assert((await dc.rpc.getConfig(acc, "ui.chat_bg")) == "color:red");
await dc.rpc.setConfig(accountId, "ui.chat_bg", "color:red");
assert((await dc.rpc.getConfig(accountId, "ui.chat_bg")) == "color:red");
});
it("set and retrive (batch)", async function () {
const config = { addr: "valid@email", mail_pw: "1234" };
await dc.rpc.batchSetConfig(acc, config);
const retrieved = await dc.rpc.batchGetConfig(acc, Object.keys(config));
await dc.rpc.batchSetConfig(accountId, config);
const retrieved = await dc.rpc.batchGetConfig(accountId, Object.keys(config));
expect(retrieved).to.deep.equal(config);
});
it("set and retrive ui.* (batch)", async function () {
@@ -139,8 +136,8 @@ describe("basic tests", () => {
"ui.chat_bg": "color:green",
"ui.enter_key_sends": "true",
};
await dc.rpc.batchSetConfig(acc, config);
const retrieved = await dc.rpc.batchGetConfig(acc, Object.keys(config));
await dc.rpc.batchSetConfig(accountId, config);
const retrieved = await dc.rpc.batchGetConfig(accountId, Object.keys(config));
expect(retrieved).to.deep.equal(config);
});
it("set and retrive mixed(ui and core) (batch)", async function () {
@@ -150,8 +147,8 @@ describe("basic tests", () => {
addr: "valid2@email",
mail_pw: "123456",
};
await dc.rpc.batchSetConfig(acc, config);
const retrieved = await dc.rpc.batchGetConfig(acc, Object.keys(config));
await dc.rpc.batchSetConfig(accountId, config);
const retrieved = await dc.rpc.batchGetConfig(accountId, Object.keys(config));
expect(retrieved).to.deep.equal(config);
});
});

View File

@@ -1,18 +1,19 @@
import { assert, expect } from "chai";
import { Deltachat, DeltachatEvent, EventTypeName } from "../dist/deltachat.js";
import { Deltachat, DeltachatEvent, EventTypeName } from "../deltachat.js";
import {
CMD_API_Server_Handle,
CMD_API_SERVER_PORT,
RpcServerHandle,
createTempUser,
startCMD_API_Server,
startServer,
} from "./test_base.js";
const EVENT_TIMEOUT = 10000
describe("online tests", function () {
let server_handle: CMD_API_Server_Handle;
let serverHandle: RpcServerHandle;
let dc: Deltachat;
let account: { email: string; password: string };
let account1: { email: string; password: string };
let account2: { email: string; password: string };
let acc1: number, acc2: number;
let accountId1: number, accountId2: number;
before(async function () {
this.timeout(12000)
@@ -29,17 +30,15 @@ describe("online tests", function () {
);
this.skip();
}
server_handle = await startCMD_API_Server(CMD_API_SERVER_PORT);
dc = new Deltachat({
url: "ws://localhost:" + CMD_API_SERVER_PORT + "/ws",
});
serverHandle = await startServer();
dc = new Deltachat(serverHandle.url)
dc.on("ALL", ({ id, contextId }) => {
if (id !== "Info") console.log(contextId, id);
});
account = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
if (!account || !account.email || !account.password) {
account1 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
if (!account1 || !account1.email || !account1.password) {
console.log(
"We didn't got back an account from the api, skip intergration tests"
);
@@ -47,6 +46,7 @@ describe("online tests", function () {
}
account2 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
console.log({ account: account1, account2 })
if (!account2 || !account2.email || !account2.password) {
console.log(
"We didn't got back an account2 from the api, skip intergration tests"
@@ -57,108 +57,109 @@ describe("online tests", function () {
after(async () => {
dc && dc.close();
server_handle && (await server_handle.close());
serverHandle && (await serverHandle.close());
});
let are_configured = false;
let accountsConfigured = false;
it("configure test accounts", async function () {
this.timeout(20000);
acc1 = await dc.rpc.addAccount();
await dc.rpc.setConfig(acc1, "addr", account.email);
await dc.rpc.setConfig(acc1, "mail_pw", account.password);
let configure_promise = dc.rpc.configure(acc1);
accountId1 = await dc.rpc.addAccount();
await dc.rpc.setConfig(accountId1, "addr", account1.email);
await dc.rpc.setConfig(accountId1, "mail_pw", account1.password);
console.log('config set')
await dc.rpc.configure(accountId1);
console.log('account configured')
acc2 = await dc.rpc.addAccount();
await dc.rpc.batchSetConfig(acc2, {
accountId2 = await dc.rpc.addAccount();
await dc.rpc.batchSetConfig(accountId2, {
addr: account2.email,
mail_pw: account2.password,
});
await Promise.all([configure_promise, dc.rpc.configure(acc2)]);
are_configured = true;
await dc.rpc.configure(accountId2)
accountsConfigured = true;
});
it("send and recieve text message", async function () {
if (!are_configured) {
if (!accountsConfigured) {
this.skip();
}
this.timeout(15000);
const contactId = await dc.rpc.contactsCreateContact(
acc1,
accountId1,
account2.email,
null
);
const chatId = await dc.rpc.contactsCreateChatByContactId(acc1, contactId);
const chatId = await dc.rpc.contactsCreateChatByContactId(accountId1, contactId);
const eventPromise = Promise.race([
waitForEvent(dc, "MsgsChanged", acc2),
waitForEvent(dc, "IncomingMsg", acc2),
waitForEvent(dc, "MsgsChanged", accountId2),
waitForEvent(dc, "IncomingMsg", accountId2),
]);
dc.rpc.miscSendTextMessage(acc1, "Hello", chatId);
await dc.rpc.miscSendTextMessage(accountId1, "Hello", chatId);
const { field1: chatIdOnAccountB } = await eventPromise;
await dc.rpc.acceptChat(acc2, chatIdOnAccountB);
await dc.rpc.acceptChat(accountId2, chatIdOnAccountB);
const messageList = await dc.rpc.messageListGetMessageIds(
acc2,
accountId2,
chatIdOnAccountB,
0
);
expect(messageList).have.length(1);
const message = await dc.rpc.messageGetMessage(acc2, messageList[0]);
const message = await dc.rpc.messageGetMessage(accountId2, messageList[0]);
expect(message.text).equal("Hello");
});
it("send and recieve text message roundtrip, encrypted on answer onwards", async function () {
if (!are_configured) {
if (!accountsConfigured) {
this.skip();
}
this.timeout(10000);
// send message from A to B
const contactId = await dc.rpc.contactsCreateContact(
acc1,
accountId1,
account2.email,
null
);
const chatId = await dc.rpc.contactsCreateChatByContactId(acc1, contactId);
const chatId = await dc.rpc.contactsCreateChatByContactId(accountId1, contactId);
const eventPromise = Promise.race([
waitForEvent(dc, "MsgsChanged", acc2),
waitForEvent(dc, "IncomingMsg", acc2),
waitForEvent(dc, "MsgsChanged", accountId2),
waitForEvent(dc, "IncomingMsg", accountId2),
]);
dc.rpc.miscSendTextMessage(acc1, "Hello2", chatId);
dc.rpc.miscSendTextMessage(accountId1, "Hello2", chatId);
// wait for message from A
console.log("wait for message from A");
const event = await eventPromise;
const { field1: chatIdOnAccountB } = event;
await dc.rpc.acceptChat(acc2, chatIdOnAccountB);
await dc.rpc.acceptChat(accountId2, chatIdOnAccountB);
const messageList = await dc.rpc.messageListGetMessageIds(
acc2,
accountId2,
chatIdOnAccountB,
0
);
const message = await dc.rpc.messageGetMessage(
acc2,
accountId2,
messageList.reverse()[0]
);
expect(message.text).equal("Hello2");
// Send message back from B to A
const eventPromise2 = Promise.race([
waitForEvent(dc, "MsgsChanged", acc1),
waitForEvent(dc, "IncomingMsg", acc1),
waitForEvent(dc, "MsgsChanged", accountId1),
waitForEvent(dc, "IncomingMsg", accountId1),
]);
dc.rpc.miscSendTextMessage(acc2, "super secret message", chatId);
dc.rpc.miscSendTextMessage(accountId2, "super secret message", chatId);
// Check if answer arives at A and if it is encrypted
await eventPromise2;
const messageId = (
await dc.rpc.messageListGetMessageIds(acc1, chatId, 0)
await dc.rpc.messageListGetMessageIds(accountId1, chatId, 0)
).reverse()[0];
const message2 = await dc.rpc.messageGetMessage(acc1, messageId);
const message2 = await dc.rpc.messageGetMessage(accountId1, messageId);
expect(message2.text).equal("super secret message");
expect(message2.show_padlock).equal(true);
});
@@ -181,23 +182,24 @@ describe("online tests", function () {
});
});
type event_data = {
contextId: number;
id: EventTypeName;
[key: string]: any;
};
async function waitForEvent(
dc: Deltachat,
event: EventTypeName,
accountId: number
): Promise<event_data> {
return new Promise((res, rej) => {
const callback = (ev: DeltachatEvent) => {
if (ev.contextId == accountId) {
dc.off(event, callback);
res(ev);
eventType: EventTypeName,
accountId: number,
timeout: number = EVENT_TIMEOUT
): Promise<DeltachatEvent> {
return new Promise((resolve, reject) => {
const rejectTimeout = setTimeout(
() => reject(new Error('Timeout reached before event came in')),
timeout
)
const callback = (event: DeltachatEvent) => {
if (event.contextId == accountId) {
dc.off(eventType, callback);
clearTimeout(rejectTimeout)
resolve(event);
}
};
dc.on(event, callback);
dc.on(eventType, callback);
});
}

View File

@@ -1,28 +1,90 @@
import { tmpdir } from "os";
import { join } from "path";
import { join, resolve } from "path";
import { mkdtemp, rm } from "fs/promises";
import { existsSync } from "fs";
import { spawn, exec } from "child_process";
import { unwrapPromise } from "./ts_helpers.js";
import fetch from "node-fetch";
/* port is not configurable yet */
export const RPC_SERVER_PORT = 20808;
export type RpcServerHandle = {
url: string,
close: () => Promise<void>
}
export async function startServer(port: number = RPC_SERVER_PORT): Promise<RpcServerHandle> {
const tmpDir = await mkdtemp(join(tmpdir(), "deltachat-jsonrpc-test"));
const pathToServerBinary = resolve(join(await getTargetDir(), "debug/deltachat-jsonrpc-server"));
console.log('using server binary: ' + pathToServerBinary);
if (!existsSync(pathToServerBinary)) {
throw new Error(
"server executable does not exist, you need to build it first" +
"\nserver executable not found at " +
pathToServerBinary
);
}
const server = spawn(pathToServerBinary, {
cwd: tmpDir,
env: {
RUST_LOG: process.env.RUST_LOG || "info",
DC_PORT: '' + port
},
});
let shouldClose = false;
server.on("exit", () => {
if (shouldClose) {
return;
}
throw new Error("Server quit");
});
server.stderr.pipe(process.stderr);
server.stdout.pipe(process.stdout)
const url = `ws://localhost:${port}/ws`
return {
url,
close: async () => {
shouldClose = true;
if (!server.kill()) {
console.log("server termination failed");
}
await rm(tmpDir, { recursive: true });
},
};
}
export async function createTempUser(url: string) {
const response = await fetch(url, {
method: "POST",
headers: {
"cache-control": "no-cache",
},
});
if (!response.ok) throw new Error('Received invalid response')
return response.json();
}
function getTargetDir(): Promise<string> {
return new Promise((res, rej) => {
return new Promise((resolve, reject) => {
exec(
"cargo metadata --no-deps --format-version 1",
(error, stdout, stderr) => {
(error, stdout, _stderr) => {
if (error) {
console.log("error", error);
rej(error);
reject(error);
} else {
try {
const json = JSON.parse(stdout);
res(json.target_directory);
resolve(json.target_directory);
} catch (error) {
console.log("json error", error);
rej(error);
reject(error);
}
}
}
@@ -30,66 +92,3 @@ function getTargetDir(): Promise<string> {
});
}
export const CMD_API_SERVER_PORT = 20808;
export async function startCMD_API_Server(port: typeof CMD_API_SERVER_PORT) {
const tmp_dir = await mkdtemp(join(tmpdir(), "test_prefix"));
const path_of_server = join(await getTargetDir(), "debug/webserver");
console.log(path_of_server);
if (!existsSync(path_of_server)) {
throw new Error(
"server executable does not exist, you need to build it first" +
"\nserver executable not found at " +
path_of_server
);
}
const server = spawn(path_of_server, {
cwd: tmp_dir,
env: {
RUST_LOG: "info",
},
});
let should_close = false;
server.on("exit", () => {
if (should_close) {
return;
}
throw new Error("Server quit");
});
server.stderr.pipe(process.stderr);
//server.stdout.pipe(process.stdout)
return {
close: async () => {
should_close = true;
if (!server.kill(9)) {
console.log("server termination failed");
}
await rm(tmp_dir, { recursive: true });
},
};
}
export type CMD_API_Server_Handle = unwrapPromise<
ReturnType<typeof startCMD_API_Server>
>;
export async function createTempUser(url: string) {
async function postData(url = "") {
// Default options are marked with *
const response = await fetch(url, {
method: "POST", // *GET, POST, PUT, DELETE, etc.
headers: {
"cache-control": "no-cache",
},
});
return response.json(); // parses JSON response into native JavaScript objects
}
return await postData(url);
}

View File

@@ -1 +0,0 @@
export type unwrapPromise<T> = T extends Promise<infer U> ? U : never;

View File

@@ -1,17 +0,0 @@
{
"compilerOptions": {
"rootDir": ".",
"outDir": "../test_dist",
"target": "ES2020",
"module": "es2020",
"moduleResolution": "node",
"declaration": false,
"esModuleInterop": true,
"noImplicitAny": true,
"isolatedModules": true,
"strictNullChecks": true,
"strict": true,
"sourceMap": true
},
"compileOnSave": true
}

View File

@@ -8,13 +8,13 @@
"outDir": "dist",
"lib": ["ES2017", "dom"],
"target": "ES2017",
"module": "es2015",
"module": "es2020",
"declaration": true,
"esModuleInterop": true,
"moduleResolution": "node",
"noImplicitAny": true,
"isolatedModules": true
},
"include": ["*.ts", "example/*.ts"],
"include": ["*.ts", "example/*.ts", "test/*.ts"],
"compileOnSave": false
}