UNPKG

atlassian-connect-express

Version:

Library for building Atlassian Add-ons on top of Express

528 lines (483 loc) 15 kB
const jwt = require("atlassian-jwt"); const nock = require("nock"); const extend = require("extend"); const _ = require("lodash"); const helper = require("./test_helper"); const mocks = require("./mocks"); const config = require("../lib/internal/config"); const HostRequest = require("../lib/internal/host-request"); describe("Host Request", () => { const clientSettings = { clientKey: "test-client-key", oauthClientId: "oauth-client-id", sharedSecret: "shared-secret", baseUrl: "https://test.atlassian.net" }; const createAddonConfig = function (opts) { opts = extend( { jwt: { validityInMinutes: 3 } }, opts ); return config({}, "development", { development: opts }); }; const mockAddon = function (addonConfig) { if (!addonConfig) { addonConfig = {}; } return { logger: require("./logger"), key: "test-addon-key", config: createAddonConfig(addonConfig), descriptor: { scopes: ["READ", "WRITE"] }, settings: mocks.store(clientSettings, clientSettings.clientKey) }; }; function getHttpClient(addonConfig, context) { if (arguments.length === 0) { addonConfig = context = {}; } else if (arguments.length === 1) { context = addonConfig; addonConfig = {}; } const a = mockAddon(addonConfig); return new HostRequest(a, context, clientSettings.clientKey); } function interceptRequest(testCallback, replyCallback, options) { const opts = extend( { baseUrl: clientSettings.baseUrl, method: "get", path: "/some/path/on/host", httpClientContext: {} }, options || {} ); if (!opts.requestPath) { opts.requestPath = opts.path; } if (!opts.uri) { opts.uri = opts.requestPath; } let interceptor = nock(opts.baseUrl)[opts.method](opts.path); if (opts.qs) { interceptor = interceptor.query(opts.qs); } interceptor = interceptor.reply(replyCallback); let httpClient = getHttpClient(opts.addonConfig, opts.httpClientContext); if (opts.httpClientWrapper) { httpClient = opts.httpClientWrapper(httpClient); } const httpClientOpts = _.cloneDeep(opts); delete httpClientOpts.baseUrl; delete httpClientOpts.method; delete httpClientOpts.path; delete httpClientOpts.requestPath; delete httpClientOpts.httpClientContext; httpClient[opts.method](httpClientOpts, () => { interceptor.done(); // will throw assertion if endpoint is not intercepted testCallback(); }); } function interceptRequestAsUser(testCallback, replyCallback, options) { const userKey = options.userKey; delete options.userKey; const opts = extend({}, options, { httpClientWrapper(httpClient) { return httpClient.asUser(userKey); } }); interceptRequest(testCallback, replyCallback, opts); } function interceptRequestAsUserByAccountId( testCallback, replyCallback, options ) { const userAccountId = options.userAccountId; delete options.userAccountId; const opts = extend({}, options, { httpClientWrapper(httpClient) { return httpClient.asUserByAccountId(userAccountId); } }); interceptRequest(testCallback, replyCallback, opts); } it("constructs non-null get request", () => { return new Promise(done => { interceptRequest(done, 200); }); }); describe("Headers", () => { it("get request has headers", () => { return new Promise(done => { // eslint-disable-next-line no-unused-vars interceptRequest(done, function (uri, requestBody) { expect(this.req.headers).toBeDefined(); }); }); }); it("get request has user-agent header", () => { return new Promise(done => { // eslint-disable-next-line no-unused-vars interceptRequest(done, function (uri, requestBody) { expect( this.req.headers["user-agent"].startsWith( "atlassian-connect-express/" ) ).toBe(true); }); }); }); it("get request has user-agent version set to package version", () => { return new Promise(done => { const aceVersion = require("../package.json").version; // eslint-disable-next-line no-unused-vars interceptRequest(done, function (uri, requestBody) { expect( this.req.headers["user-agent"].startsWith( `atlassian-connect-express/${aceVersion}` ) ).toBe(true); }); }); }); it("get request has custom user-agent", () => { return new Promise(done => { const userAgent = "my-fun-app"; const opts = { addonConfig: { userAgent } }; interceptRequest( done, // eslint-disable-next-line no-unused-vars function (uri, requestBody) { expect(this.req.headers["user-agent"]).toBe(userAgent); }, opts ); }); }); it("post request preserves custom header", () => { return new Promise(done => { const interceptor = nock(clientSettings.baseUrl) .post("/some/path") // eslint-disable-next-line no-unused-vars .reply(function (uri, requestBody) { expect(this.req.headers.custom_header).toBe("arbitrary value"); }); getHttpClient().post( { url: "/some/path", headers: { custom_header: "arbitrary value" } }, () => { interceptor.done(); done(); } ); }); }); }); describe("Add-on JWT authentication", () => { it("get request has Authorization header", () => { return new Promise(done => { // eslint-disable-next-line no-unused-vars interceptRequest(done, function (uri, requestBody) { expect(this.req.headers.authorization).toBeDefined(); }); }); }); it("bitbucket request sets sub claim as clientKey", () => { return new Promise(done => { // eslint-disable-next-line no-unused-vars interceptRequest( done, function () { const jwtToken = this.req.headers.authorization.slice(4); const clientKey = clientSettings.clientKey; const decoded = jwt.decode(jwtToken, clientKey, true); expect(decoded.sub).toEqual(clientKey); }, { addonConfig: { product: "bitbucket" } } ); }); }); it('get request has Authorization header starting with "JWT "', () => { return new Promise(done => { // eslint-disable-next-line no-unused-vars interceptRequest(done, function (uri, requestBody) { expect(this.req.headers.authorization.startsWith("JWT ")).toBe(true); }); }); }); it("get request has correct JWT qsh for encoded parameter", () => { return new Promise(done => { // eslint-disable-next-line no-unused-vars interceptRequest( done, function () { const jwtToken = this.req.headers.authorization.slice(4); const decoded = jwt.decode( jwtToken, clientSettings.clientKey, true ); const expectedQsh = jwt.createQueryStringHash( jwt.fromExpressRequest({ method: "GET", path: "/some/path/on/host", query: { q: "~ text" } }), false, helper.productBaseUrl ); expect(decoded.qsh).toEqual(expectedQsh); }, { path: "/some/path/on/host?q=~%20text" } ); }); }); it("get request has correct JWT qsh for encoded parameter passed via qs field", () => { return new Promise(done => { const query = { q: "~ text" }; // eslint-disable-next-line no-unused-vars interceptRequest( done, function () { const jwtToken = this.req.headers.authorization.slice(4); const decoded = jwt.decode( jwtToken, clientSettings.clientKey, true ); const expectedQsh = jwt.createQueryStringHash( jwt.fromExpressRequest({ method: "GET", path: "/some/path/on/host", query }), false, helper.productBaseUrl ); expect(decoded.qsh).toEqual(expectedQsh); }, { path: "/some/path/on/host", qs: query } ); }); }); it("get request for absolute url on host has Authorization header", () => { return new Promise(done => { // eslint-disable-next-line no-unused-vars interceptRequest( done, function () { expect(this.req.headers.authorization.startsWith("JWT ")).toBe( true ); }, { requestPath: "https://test.atlassian.net/some/path/on/host" } ); }); }); it("post request has correct url", () => { return new Promise(done => { // eslint-disable-next-line no-unused-vars interceptRequest( done, function () { expect(this.req.headers.authorization.startsWith("JWT ")).toBe( true ); }, { method: "post" } ); }); }); }); describe("User impersonation requests", () => { it("Request as user does not add JWT authorization header", () => { return new Promise(done => { const authServiceMock = mocks.oauth2.service(); // eslint-disable-next-line no-unused-vars interceptRequestAsUser( done, function () { authServiceMock.done(); expect(this.req.headers.authorization.startsWith("JWT")).toBe( false ); }, { userKey: "sruiz" } ); }); }); it("Request as user adds a Bearer authorization header", () => { return new Promise(done => { const authServiceMock = mocks.oauth2.service(); // eslint-disable-next-line no-unused-vars interceptRequestAsUser( done, function () { authServiceMock.done(); expect(this.req.headers.authorization.startsWith("Bearer")).toBe( true ); }, { userKey: "sruiz" } ); }); }); it("Request as user adds a Bearer authorization header when using account id", () => { return new Promise(done => { const authServiceMock = mocks.oauth2.service(); // eslint-disable-next-line no-unused-vars interceptRequestAsUserByAccountId( done, function () { authServiceMock.done(); expect(this.req.headers.authorization.startsWith("Bearer")).toBe( true ); }, { userAccountId: "048abaf9-04ea-44d1-acb9-b37de6cc5d2f" } ); }); }); }); describe("Form requests", () => { it("post request with form sets form data", () => { return new Promise(done => { // eslint-disable-next-line no-unused-vars const interceptor = nock(clientSettings.baseUrl) .post("/some/path") .reply(200); getHttpClient() .post({ url: "/some/path", file: [ "file content", { filename: "filename", ContentType: "text/plain" } ] }) .then(request => { expect(request.file).toEqual([ "file content", { filename: "filename", ContentType: "text/plain" } ]); done(); }); }); }); it("post requests using multipartFormData have the right format", () => { return new Promise(done => { // eslint-disable-next-line no-unused-vars const interceptor = nock(clientSettings.baseUrl) .post("/some/path") .reply(200); const someData = "some data"; getHttpClient() .post({ url: "/some/path", multipartFormData: { file: [someData, { filename: "myattachmentagain.png" }] } }) .then(request => { expect(request._form._valueLength).toEqual(someData.length); done(); }); }); }); it("post requests using the deprecated form parameter still have the right format", () => { return new Promise(done => { // eslint-disable-next-line no-unused-vars const interceptor = nock(clientSettings.baseUrl) .post("/some/path") .reply(200); const someData = "some data"; getHttpClient() .post({ url: "/some/path", form: { file: [someData, { filename: "myattachmentagain.png" }] } }) .then( request => { expect(request._form._valueLength).toEqual(someData.length); done(); }, err => { console.log(err); } ); }); }); it("post requests using urlEncodedFormData have the right format", () => { return new Promise(done => { // eslint-disable-next-line no-unused-vars const interceptor = nock(clientSettings.baseUrl) .post("/some/path") .reply(200); getHttpClient() .post({ url: "/some/path", urlEncodedFormData: { param1: "value1" } }) .then(request => { expect(request.body.toString()).toBe("param1=value1"); done(); }); }); }); it("post request with undefined clientKey returns promise reject", () => { return new Promise(done => { // eslint-disable-next-line no-unused-vars const interceptor = nock(clientSettings.baseUrl) .post("/some/path") .reply(200); new HostRequest(mockAddon({}), {}, undefined) .post({ url: "/some/path", urlEncodedFormData: { param1: "value1" } }) .then( () => { // Promise is resolved done(new Error("Promise should not be resolved")); }, // eslint-disable-next-line no-unused-vars reason => { // Promise is rejected done(); } ); }); }); }); });