feat: init
This commit is contained in:
@@ -0,0 +1,351 @@
|
||||
import { afterAll, beforeAll, describe, expect, it } from "@jest/globals";
|
||||
import { init, TestPlatform } from "../setup";
|
||||
import { TestDbService } from "../utils.prepare.db";
|
||||
import { Api } from "../utils.api";
|
||||
import Application, {
|
||||
PublicApplicationObject,
|
||||
} from "../../../src/services/applications/entities/application";
|
||||
import { cloneDeep } from "lodash";
|
||||
|
||||
import { logger as log } from "../../../src/core/platform/framework";
|
||||
import { v1 as uuidv1 } from "uuid";
|
||||
|
||||
describe("Applications", () => {
|
||||
const url = "/internal/services/applications/v1";
|
||||
|
||||
let platform: TestPlatform;
|
||||
let testDbService: TestDbService;
|
||||
let api: Api;
|
||||
let appRepo;
|
||||
|
||||
beforeAll(async ends => {
|
||||
platform = await init();
|
||||
await platform.database.getConnector().drop();
|
||||
testDbService = await TestDbService.getInstance(platform, true);
|
||||
await testDbService.createDefault();
|
||||
postPayload.company_id = platform.workspace.company_id;
|
||||
api = new Api(platform);
|
||||
appRepo = await testDbService.getRepository("application", Application);
|
||||
ends();
|
||||
});
|
||||
|
||||
afterAll(done => {
|
||||
platform.tearDown().then(() => done());
|
||||
});
|
||||
|
||||
const publishApp = async id => {
|
||||
const entity = await appRepo.findOne({ id });
|
||||
if (!entity) throw new Error(`entity ${id} not found`);
|
||||
entity.publication.published = true;
|
||||
await appRepo.save(entity);
|
||||
};
|
||||
|
||||
describe("Create application", function () {
|
||||
it("should 403 if creator is not a company admin", async done => {
|
||||
const payload = { resource: cloneDeep(postPayload) };
|
||||
|
||||
const user = await testDbService.createUser([testDbService.defaultWorkspace()], {
|
||||
companyRole: "member",
|
||||
});
|
||||
|
||||
const response = await api.post(`${url}/applications`, payload, user.id);
|
||||
expect(response.statusCode).toBe(403);
|
||||
done();
|
||||
});
|
||||
|
||||
it("should 200 on application create", async done => {
|
||||
const payload = { resource: cloneDeep(postPayload) };
|
||||
const response = await api.post(`${url}/applications`, payload);
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
const r = response.resource;
|
||||
|
||||
expect(r.company_id).toBe(payload.resource.company_id);
|
||||
expect(!!r.is_default).toBe(false);
|
||||
expect(r.identity).toMatchObject(payload.resource.identity);
|
||||
expect(r.access).toMatchObject(payload.resource.access);
|
||||
expect(r.display).toMatchObject(payload.resource.display);
|
||||
expect(r.publication).toMatchObject(payload.resource.publication);
|
||||
expect(r.stats).toMatchObject({
|
||||
created_at: expect.any(Number),
|
||||
updated_at: expect.any(Number),
|
||||
version: 0,
|
||||
});
|
||||
|
||||
expect(r.api).toMatchObject({
|
||||
hooks_url: payload.resource.api.hooks_url,
|
||||
allowed_ips: payload.resource.api.allowed_ips,
|
||||
private_key: expect.any(String),
|
||||
});
|
||||
|
||||
expect(r.api.private_key).not.toBe("");
|
||||
|
||||
const dbData = await appRepo.findOne({ id: response.resource.id });
|
||||
|
||||
expect(dbData.api).toMatchObject({
|
||||
allowed_ips: payload.resource.api.allowed_ips,
|
||||
hooks_url: payload.resource.api.hooks_url,
|
||||
private_key: expect.any(String),
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
describe("Update application", function () {
|
||||
let createdApp: PublicApplicationObject;
|
||||
|
||||
beforeAll(async done => {
|
||||
const payload = { resource: cloneDeep(postPayload) };
|
||||
const response = await api.post(`${url}/applications`, payload);
|
||||
createdApp = response.resource;
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it("should 403 if editor is not a company admin", async done => {
|
||||
if (!createdApp) throw new Error("can't find created app");
|
||||
log.debug(createdApp);
|
||||
|
||||
const user = await testDbService.createUser([testDbService.defaultWorkspace()], {
|
||||
companyRole: "member",
|
||||
});
|
||||
|
||||
const response = await api.post(`${url}/applications/${createdApp.id}`, postPayload, user.id);
|
||||
expect(response.statusCode).toBe(403);
|
||||
done();
|
||||
});
|
||||
|
||||
it("should 404 if application not found", async done => {
|
||||
const response = await api.post(`${url}/applications/${uuidv1()}`, { resource: postPayload });
|
||||
expect(response.statusCode).toBe(404);
|
||||
done();
|
||||
});
|
||||
|
||||
describe("Unpublished application", () => {
|
||||
it("should 200 on application update", async done => {
|
||||
const payload = { resource: cloneDeep(postPayload) };
|
||||
|
||||
payload.resource.is_default = true;
|
||||
payload.resource.identity.name = "test2";
|
||||
payload.resource.api.hooks_url = "123123";
|
||||
payload.resource.access.read = [];
|
||||
payload.resource.publication.requested = true;
|
||||
|
||||
const response = await api.post(`${url}/applications/${createdApp.id}`, payload);
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
const r = response.resource;
|
||||
|
||||
expect(r.company_id).toBe(payload.resource.company_id);
|
||||
expect(!!r.is_default).toBe(false);
|
||||
expect(r.identity).toMatchObject(payload.resource.identity);
|
||||
|
||||
expect(r.access).toMatchObject(payload.resource.access);
|
||||
expect(r.display).toMatchObject(payload.resource.display);
|
||||
expect(r.publication).toMatchObject(payload.resource.publication);
|
||||
expect(r.stats).toMatchObject({
|
||||
created_at: expect.any(Number),
|
||||
updated_at: expect.any(Number),
|
||||
version: 1,
|
||||
});
|
||||
|
||||
expect(r.api).toBeTruthy();
|
||||
|
||||
const dbData = await appRepo.findOne({ id: response.resource.id });
|
||||
|
||||
expect(dbData.api).toMatchObject({
|
||||
allowed_ips: payload.resource.api.allowed_ips,
|
||||
hooks_url: payload.resource.api.hooks_url,
|
||||
private_key: expect.any(String),
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip("Published application", () => {
|
||||
beforeAll(async done => {
|
||||
const payload = { resource: cloneDeep(postPayload) };
|
||||
const response = await api.post(`${url}/applications`, payload);
|
||||
createdApp = response.resource;
|
||||
await publishApp(createdApp.id);
|
||||
done();
|
||||
});
|
||||
|
||||
it("should 200 on update if allowed fields changed", async done => {
|
||||
const payload = { resource: cloneDeep(createdApp) as Application };
|
||||
const entity = await appRepo.findOne({ id: createdApp.id });
|
||||
payload.resource.api = cloneDeep(entity.api);
|
||||
payload.resource.publication.requested = true;
|
||||
const response = await api.post(`${url}/applications/${createdApp.id}`, payload);
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
expect(response.resource.publication).toMatchObject({
|
||||
requested: true,
|
||||
published: true,
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
it("should 400 on update if not allowed fields changed", async done => {
|
||||
const payload = { resource: cloneDeep(createdApp) as Application };
|
||||
const entity = await appRepo.findOne({ id: createdApp.id });
|
||||
payload.resource.api = cloneDeep(entity.api);
|
||||
const response = await api.post(`${url}/applications/${createdApp.id}`, payload);
|
||||
expect(response.statusCode).toBe(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("Get applications", function () {
|
||||
let firstApp: PublicApplicationObject;
|
||||
let secondApp: PublicApplicationObject;
|
||||
let thirdApp: PublicApplicationObject;
|
||||
beforeAll(async done => {
|
||||
const payload = { resource: cloneDeep(postPayload) };
|
||||
firstApp = (await api.post(`${url}/applications`, payload)).resource;
|
||||
secondApp = (await api.post(`${url}/applications`, payload)).resource;
|
||||
thirdApp = (await api.post(`${url}/applications`, payload)).resource;
|
||||
|
||||
await publishApp(firstApp.id);
|
||||
await publishApp(secondApp.id);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it("should list published applications", async done => {
|
||||
const response = await api.get(`${url}/applications`);
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
const published = response.resources.filter(a => a.publication.published).length;
|
||||
const unpublished = response.resources.filter(a => a.publication.unpublished).length;
|
||||
|
||||
expect(published).toBeGreaterThanOrEqual(2);
|
||||
expect(unpublished).toEqual(0);
|
||||
|
||||
expect(response.resources.map(a => a.id)).toEqual(
|
||||
expect.arrayContaining([firstApp.id, secondApp.id]),
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it("should return public object for published application to any user", async done => {
|
||||
const response = await api.get(`${url}/applications/${firstApp.id}`, uuidv1());
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.resource.id).toEqual(firstApp.id);
|
||||
expect(response.resource.api).toBeFalsy();
|
||||
done();
|
||||
});
|
||||
|
||||
it("should return public object for unpublished application to any user", async done => {
|
||||
const response = await api.get(`${url}/applications/${thirdApp.id}`, uuidv1());
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.resource.id).toEqual(thirdApp.id);
|
||||
expect(response.resource.api).toBeFalsy();
|
||||
done();
|
||||
});
|
||||
|
||||
it("should return whole object for published application to admin", async done => {
|
||||
const response = await api.get(`${url}/applications/${firstApp.id}`);
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.resource.id).toEqual(firstApp.id);
|
||||
expect(response.resource.api).toBeTruthy();
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it("should return whole object for unpublished application to admin", async done => {
|
||||
const response = await api.get(`${url}/applications/${thirdApp.id}`);
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.resource.id).toEqual(thirdApp.id);
|
||||
expect(response.resource.api).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const postPayload = {
|
||||
is_default: true,
|
||||
company_id: null,
|
||||
identity: {
|
||||
code: "code",
|
||||
name: "name",
|
||||
icon: "icon",
|
||||
description: "description",
|
||||
website: "website",
|
||||
categories: [],
|
||||
compatibility: [],
|
||||
},
|
||||
api: {
|
||||
hooks_url: "hooks_url",
|
||||
allowed_ips: "allowed_ips",
|
||||
},
|
||||
access: {
|
||||
read: ["messages"],
|
||||
write: ["messages"],
|
||||
delete: ["messages"],
|
||||
hooks: ["messages"],
|
||||
},
|
||||
display: {
|
||||
twake: {
|
||||
version: 1,
|
||||
|
||||
files: {
|
||||
editor: {
|
||||
preview_url: "string", //Open a preview inline (iframe)
|
||||
edition_url: "string", //Url to edit the file (full screen)
|
||||
extensions: [], //Main extensions app can read
|
||||
// if file was created by the app, then the app is able to edit with or without extension
|
||||
empty_files: [
|
||||
{
|
||||
url: "string", // "https://[...]/empty.docx";
|
||||
filename: "string", // "Untitled.docx";
|
||||
name: "string", // "Word Document";
|
||||
},
|
||||
],
|
||||
},
|
||||
actions: [
|
||||
//List of action that can apply on a file
|
||||
{
|
||||
name: "string",
|
||||
id: "string",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
//Chat plugin
|
||||
chat: {
|
||||
input: true,
|
||||
commands: [
|
||||
{
|
||||
command: "string", // my_app mycommand
|
||||
description: "string",
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
//List of action that can apply on a message
|
||||
{
|
||||
name: "string",
|
||||
id: "string",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
//Allow app to appear as a bot user in direct chat
|
||||
direct: false,
|
||||
|
||||
//Display app as a standalone application in a tab
|
||||
tab: { url: "string" },
|
||||
|
||||
//Display app as a standalone application on the left bar
|
||||
standalone: { url: "string" },
|
||||
|
||||
//Define where the app can be configured from
|
||||
configuration: ["global", "channel"],
|
||||
},
|
||||
},
|
||||
publication: {
|
||||
requested: false, //Publication requested
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
import * as crypto from "crypto";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "@jest/globals";
|
||||
import { FastifyInstance, FastifyPluginCallback, FastifyReply, FastifyRequest } from "fastify";
|
||||
|
||||
import { init, TestPlatform } from "../setup";
|
||||
import { TestDbService } from "../utils.prepare.db";
|
||||
import { Api } from "../utils.api";
|
||||
|
||||
import { logger as log } from "../../../src/core/platform/framework";
|
||||
|
||||
let signingSecret = "";
|
||||
|
||||
describe("Application events", () => {
|
||||
const url = "/internal/services/applications/v1";
|
||||
let platform: TestPlatform;
|
||||
let api: Api;
|
||||
let appId: string;
|
||||
|
||||
beforeAll(async ends => {
|
||||
platform = await init(undefined, testAppHookRoute);
|
||||
|
||||
await platform.database.getConnector().drop();
|
||||
|
||||
await TestDbService.getInstance(platform, true);
|
||||
api = new Api(platform);
|
||||
|
||||
postPayload.company_id = platform.workspace.company_id;
|
||||
|
||||
const createdApplication = await api.post("/internal/services/applications/v1/applications", {
|
||||
resource: postPayload,
|
||||
});
|
||||
|
||||
appId = createdApplication.resource.id;
|
||||
signingSecret = createdApplication.resource.api.private_key;
|
||||
|
||||
ends();
|
||||
|
||||
afterAll(done => {
|
||||
platform.tearDown().then(() => done());
|
||||
});
|
||||
});
|
||||
|
||||
it("Should 200 on sending well formed event", async done => {
|
||||
const payload = {
|
||||
company_id: platform.workspace.company_id,
|
||||
workspace_id: platform.workspace.workspace_id,
|
||||
type: "some type",
|
||||
name: "name",
|
||||
content: {},
|
||||
};
|
||||
|
||||
const response = await api.post(`${url}/applications/${appId}/event`, payload);
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.resource).toMatchObject({ a: "b" });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
const postPayload = {
|
||||
is_default: false,
|
||||
company_id: null,
|
||||
identity: {
|
||||
code: "code",
|
||||
name: "name",
|
||||
icon: "icon",
|
||||
description: "description",
|
||||
website: "website",
|
||||
categories: [],
|
||||
compatibility: [],
|
||||
},
|
||||
api: {
|
||||
hooks_url: "http://localhost:3000/test/appHook",
|
||||
allowed_ips: "allowed_ips",
|
||||
},
|
||||
access: {
|
||||
read: ["messages"],
|
||||
write: ["messages"],
|
||||
delete: ["messages"],
|
||||
hooks: ["messages"],
|
||||
},
|
||||
display: {},
|
||||
publication: {
|
||||
requested: true,
|
||||
},
|
||||
};
|
||||
|
||||
const testAppHookRoute = (fastify: FastifyInstance) => {
|
||||
const routes: FastifyPluginCallback = (fastify: FastifyInstance, options, next) => {
|
||||
fastify.route({
|
||||
method: "POST",
|
||||
url: "/test/appHook",
|
||||
handler: (request: FastifyRequest, reply: FastifyReply): Promise<any> => {
|
||||
const signature = request.headers["x-twake-signature"];
|
||||
if (!signature) {
|
||||
reply.status(403);
|
||||
reply.send({ error: "Signature is missing" });
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const expectedSignature = crypto
|
||||
.createHmac("sha256", signingSecret)
|
||||
.update(JSON.stringify(request.body))
|
||||
.digest("hex");
|
||||
if (expectedSignature != signature) {
|
||||
reply.status(403);
|
||||
reply.send({ error: "Wrong signature" });
|
||||
return undefined;
|
||||
}
|
||||
|
||||
log.debug({ signatureMatched: expectedSignature == signature });
|
||||
return Promise.resolve({ a: "b" });
|
||||
},
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
fastify.register(routes);
|
||||
};
|
||||
@@ -0,0 +1,141 @@
|
||||
import { afterAll, beforeAll, describe, expect, it } from "@jest/globals";
|
||||
|
||||
import { init, TestPlatform } from "../setup";
|
||||
import { TestDbService } from "../utils.prepare.db";
|
||||
import { Api } from "../utils.api";
|
||||
import { logger as log } from "../../../src/core/platform/framework";
|
||||
import { ApplicationLoginResponse } from "../../../src/services/applicationsapi/web/types";
|
||||
|
||||
describe("Applications", () => {
|
||||
let platform: TestPlatform;
|
||||
let testDbService: TestDbService;
|
||||
let api: Api;
|
||||
let appId: string;
|
||||
let private_key: string;
|
||||
let accessToken: ApplicationLoginResponse["access_token"];
|
||||
|
||||
beforeAll(async ends => {
|
||||
platform = await init();
|
||||
await platform.database.getConnector().drop();
|
||||
testDbService = await TestDbService.getInstance(platform, true);
|
||||
api = new Api(platform);
|
||||
|
||||
postPayload.company_id = platform.workspace.company_id;
|
||||
|
||||
const createdApplication = await api.post("/internal/services/applications/v1/applications", {
|
||||
resource: postPayload,
|
||||
});
|
||||
|
||||
appId = createdApplication.resource.id;
|
||||
private_key = createdApplication.resource.api.private_key;
|
||||
|
||||
ends();
|
||||
});
|
||||
|
||||
afterAll(done => {
|
||||
platform.tearDown().then(() => done());
|
||||
});
|
||||
|
||||
describe("Login", function () {
|
||||
it("Should be ok on valid token", async done => {
|
||||
expect(appId).toBeTruthy();
|
||||
|
||||
const response = await api.post("/api/console/v1/login", {
|
||||
id: appId,
|
||||
secret: private_key,
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
const resource = (await response.json()).resource as ApplicationLoginResponse;
|
||||
|
||||
expect(resource).toMatchObject({
|
||||
access_token: {
|
||||
time: expect.any(Number),
|
||||
expiration: expect.any(Number),
|
||||
refresh_expiration: expect.any(Number),
|
||||
value: expect.any(String),
|
||||
refresh: expect.any(String),
|
||||
type: expect.any(String),
|
||||
},
|
||||
});
|
||||
|
||||
accessToken = resource.access_token;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Get myself", function () {
|
||||
it("Should be 401 on invalid token", async done => {
|
||||
const response = await platform.app.inject({
|
||||
method: "GET",
|
||||
url: "/api/console/v1/me",
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken.value + "!!"}`,
|
||||
},
|
||||
});
|
||||
log.debug(response.json());
|
||||
expect(response.statusCode).toBe(401);
|
||||
done();
|
||||
});
|
||||
|
||||
it("Should be 403 on auth by users (not company) token", async done => {
|
||||
const userToken = await platform.auth.getJWTToken();
|
||||
|
||||
const response = await platform.app.inject({
|
||||
method: "GET",
|
||||
url: "/api/console/v1/me",
|
||||
headers: {
|
||||
authorization: `Bearer ${userToken}`,
|
||||
},
|
||||
});
|
||||
log.debug(response.json());
|
||||
expect(response.statusCode).toBe(403);
|
||||
done();
|
||||
});
|
||||
|
||||
it("Should be ok on valid token", async done => {
|
||||
const response = await platform.app.inject({
|
||||
method: "GET",
|
||||
url: "/api/console/v1/me",
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken.value}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
const resource = (await response.json()).resource;
|
||||
log.debug(resource);
|
||||
expect(resource).toMatchObject(postPayload);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const postPayload = {
|
||||
is_default: false,
|
||||
company_id: null,
|
||||
identity: {
|
||||
code: "code",
|
||||
name: "name",
|
||||
icon: "icon",
|
||||
description: "description",
|
||||
website: "website",
|
||||
categories: [],
|
||||
compatibility: [],
|
||||
},
|
||||
api: {
|
||||
hooks_url: "hooks_url",
|
||||
allowed_ips: "allowed_ips",
|
||||
},
|
||||
access: {
|
||||
read: ["messages"],
|
||||
write: ["messages"],
|
||||
delete: ["messages"],
|
||||
hooks: ["messages"],
|
||||
},
|
||||
display: {},
|
||||
publication: {
|
||||
requested: true,
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user