diff --git a/api/client.ts b/api/client.ts index d9c7c6b2c..9267232d8 100644 --- a/api/client.ts +++ b/api/client.ts @@ -11,6 +11,7 @@ import { Bakery } from "@canonical/macaroon-bakery"; import AdminV3, { AuthUserInfo, FacadeVersions, + HostPort, LoginRequest, LoginResult, RedirectInfoResult, @@ -18,6 +19,8 @@ import AdminV3, { import { Error as MacaroonError, + MacaroonJSONV1, + MacaroonJSONV2, MacaroonObject, } from "@canonical/macaroon-bakery/dist/macaroon"; import type { @@ -59,6 +62,12 @@ export interface Credentials { macaroons?: MacaroonObject[][]; } +// The type of a Macaroon from the Admin facade does not match a real macaroon. +const isMacaroonObject = ( + macaroon: unknown +): macaroon is MacaroonJSONV1 | MacaroonJSONV2 => + !!macaroon && typeof macaroon === "object"; + /** Connect to the Juju controller or model at the given URL. @@ -92,7 +101,7 @@ function connect( url: string, options?: ConnectOptions, callback?: Callback -): Promise { +): Promise { if (!options) { options = { closeCallback: () => {} }; } @@ -170,16 +179,16 @@ async function connectAndLogin( logout: typeof Client.prototype.logout; }> { // Connect to Juju. - const juju: Client = await connect(url, options); + const juju = await connect(url, options); try { const conn = await juju.login(credentials, clientVersion); return { conn, logout: juju.logout.bind(juju) }; - } catch (error: any) { - if (!juju || !juju.isRedirectionError(error)) { + } catch (error) { + if (!juju.isRedirectionError(error)) { throw error; } // Redirect to the real model. - juju && juju.logout(); + juju.logout(); for (let i = 0; i < error.servers.length; i++) { const srv = error.servers[i][0]; // TODO(frankban): we should really try to connect to all servers and @@ -187,10 +196,7 @@ async function connectAndLogin( // that the public hostname is reachable. if (srv.type === "hostname" && srv.scope === "public") { // This is a public server with a dns-name, connect to it. - const generateURL = ( - uuidOrURL: string, - srv: { value: any; port: any } - ) => { + const generateURL = (uuidOrURL: string, srv: HostPort) => { let uuid = uuidOrURL; if (uuid.startsWith("wss://") || uuid.startsWith("ws://")) { const parts = uuid.split("/"); @@ -291,7 +297,7 @@ class Client { // eslint-disable-next-line no-async-promise-executor return await new Promise(async (resolve, reject) => { - let response: any; + let response: LoginResult | null = null; try { try { response = await this._admin.login(args); @@ -344,7 +350,13 @@ class Client { ) ); }; - this._bakery.discharge(dischargeRequired, onSuccess, onFailure); + if (isMacaroonObject(dischargeRequired)) { + this._bakery.discharge(dischargeRequired, onSuccess, onFailure); + } else { + throw new Error( + "Discharge macaroon doesn't appear to be a macaroon." + ); + } return; } resolve(new Connection(this._transport, this._facades, response)); @@ -382,7 +394,7 @@ class Client { @param err The error returned by the login request. @returns Whether the given error is a redirection error. */ - isRedirectionError(err: any): boolean { + isRedirectionError(err: unknown): err is RedirectionError { return err instanceof RedirectionError; } } @@ -417,7 +429,7 @@ class RedirectionError extends Error { export class Transport { _ws: WebSocket; _counter: number; - _callbacks: { [k: number]: Callback }; + _callbacks: Record>; _closeCallback: CloseCallback; _debug: boolean; @@ -451,9 +463,9 @@ export class Transport { @param resolve Function called when the request is successful. @param reject Function called when the request is not successful. */ - write( + write( req: JujuRequest, - resolve: (value: any) => void, + resolve: (value: R) => void, reject: (error: Error) => void ) { // Check that the connection is ready and sane. @@ -468,8 +480,8 @@ export class Transport { this._counter += 1; // Include the current request id in the request. req["request-id"] = this._counter; - this._callbacks[this._counter] = (error, result) => - error ? reject(error) : resolve(result); + this._callbacks[this._counter] = (error, result: unknown) => + error ? reject(error) : resolve(result as R); const msg = JSON.stringify(req); if (this._debug) { console.debug("-->", msg); diff --git a/api/helpers.ts b/api/helpers.ts deleted file mode 100644 index b26162eec..000000000 --- a/api/helpers.ts +++ /dev/null @@ -1,416 +0,0 @@ -// Copyright 2020 Canonical Ltd. -// Licensed under the LGPLv3, see LICENSE.txt file for details. - -/** - This module contains versions of some hard to use api facade methods for - convenience purposes. -*/ - -import type { Callback, CallbackError } from "../generator/interfaces"; -import type PingerV1 from "./facades/pinger/PingerV1.js"; -import { createAsyncHandler } from "./utils.js"; - -/** - Decorate the Admin facade class. - - @param cls The auto-generated class. - @returns The decorated class. -*/ -function wrapAdmin(cls: any): object { - /** - RedirectInfo returns redirected host information for the model. In Juju it - always returns an error because the Juju controller does not multiplex - controllers. - - @param callback Called when the response from Juju is available, - the callback receives an error and the result. If there are no errors, - the result is provided as an object like the following: - { - servers: []{ - value: string, - type: string, - scope: string, - port: string - }, - caCert: string - } - */ - cls.prototype.redirectInfo = function () { - // This is overridden as the auto-generated version does not work with - // current JAAS, because the servers passed to the callback do not - // correspond to the ones declared in the API. - return new Promise((resolve, reject) => { - // Prepare the request to the Juju API. - const req = { - type: "Admin", - request: "RedirectInfo", - version: this.version, - params: {}, - }; - - const transform = (resp: { [x: string]: any; servers: any[] }) => { - // Flatten response to make it easier to work with. - const servers: any = []; - resp.servers.forEach((srvs: any) => { - srvs.forEach((srv: any) => { - const server = { - value: srv.value, - port: srv.port, - type: srv.type, - scope: srv.scope, - url: (uuidOrURL: string) => { - let uuid = uuidOrURL; - if (uuid.startsWith("wss://") || uuid.startsWith("ws://")) { - const parts = uuid.split("/"); - uuid = parts[parts.length - 2]; - } - return `wss://${srv.value}:${srv.port}/model/${uuid}/api`; - }, - }; - servers.push(server); - }); - }); - return { "ca-cert": resp["ca-cert"], servers }; - }; - - const handler = createAsyncHandler(undefined, resolve, reject, transform); - // Send the request to the server. - this._transport.write(req, handler.resolve, handler.reject); - }); - }; - - return cls; -} - -/** - Decorate the AllModelWatcher facade class. - - @param cls The auto-generated class. - @returns The decorated class. -*/ -function wrapAllModelWatcher(cls: any) { - /** - Ask for next watcher messages corresponding to changes in the models. - - @param watcherId The id of the currently used watcher. The id is - retrieved by calling the Controller.watchAllModels API call. - @param callback Called when the next messages arrive, the - callback receives an error and the changes. If there are no errors, - changes are provided as an object like the following: - { - deltas: []anything - } - @return Rejected or resolved with the values normally passed to - the callback when the callback is not provided. - This allows this method to be awaited. - */ - cls.prototype.next = function ( - watcherId: string, - callback: Callback - ): Promise { - // This method is overridden as the auto-generated one does not include the - // watcherId parameter, as a result of the peculiarity of the call, which - // does not assume the id to be in parameters, but as a top level field. - return new Promise((resolve, reject) => { - // Prepare the request to the Juju API. - const req = { - type: "AllModelWatcher", - request: "Next", - version: this.version, - id: watcherId, - }; - // Allow for js-friendly responses. - const transform = (resp: any) => { - resp = resp || {}; - return { deltas: resp.deltas || [] }; - }; - const handler = createAsyncHandler(callback, resolve, reject, transform); - // Send the request to the server. - this._transport.write(req, handler.resolve, handler.reject); - }); - }; - - /** - Stop watching all models. - - @param watcherId The id of the currently used watcher. The id is - retrieved by calling the Controller.watchAllModels API call. - @param callback Called after the watcher has been stopped, the - callback receives an error. - @return Rejected or resolved with the values normally passed to - the callback when the callback is not provided. - This allows this method to be awaited. - */ - cls.prototype.stop = function ( - watcherId: string, - callback: Callback - ): Promise { - // This method is overridden as the auto-generated one does not include the - // watcherId parameter, as a result of the peculiarity of the call, which - // does not assume the id to be in parameters, but as a top level field. - return new Promise((resolve, reject) => { - // Prepare the request to the Juju API. - const req = { - type: "AllModelWatcher", - request: "Stop", - version: this.version, - id: watcherId, - }; - const handler = createAsyncHandler(callback, resolve, reject); - // Send the request to the server. - this._transport.write(req, handler.resolve, handler.reject); - }); - }; - - return cls; -} - -/** - Decorate the AllWatcher facade class. - - @param cls The auto-generated class. - @returns The decorated class. -*/ -function wrapAllWatcher(cls: any) { - /** - Ask for next watcher messages corresponding to changes in the model. - - @param watcherId The id of the currently used watcher. The id is - retrieved by calling the Client.watchAll API call. - @param callback Called when the next messages arrive, the - callback receives an error and the changes. If there are no errors, - changes are provided as an object like the following: - { - deltas: []anything - } - @return Rejected or resolved with the values normally passed to - the callback when the callback is not provided. - This allows this method to be awaited. - */ - cls.prototype.next = function ( - watcherId: string, - callback: Callback - ): Promise { - // This method is overridden as the auto-generated one does not include the - // watcherId parameter, as a result of the peculiarity of the call, which - // does not assume the id to be in parameters, but as a top level field. - return new Promise((resolve, reject) => { - // Prepare the request to the Juju API. - const req = { - type: "AllWatcher", - request: "Next", - version: this.version, - id: watcherId, - }; - // Allow for js-friendly responses. - const transform = (resp: any) => { - resp = resp || {}; - return { deltas: resp.deltas || [] }; - }; - const handler = createAsyncHandler(callback, resolve, reject, transform); - // Send the request to the server. - this._transport.write(req, handler.resolve, handler.reject); - }); - }; - - /** - Stop watching the model. - - @param watcherId The id of the currently used watcher. The id is - retrieved by calling the Client.watchAll API call. - @param callback Called after the watcher has been stopped, the - callback receives an error. - @return Rejected or resolved with the values normally passed to - the callback when the callback is not provided. - This allows this method to be awaited. - */ - cls.prototype.stop = function ( - watcherId: string, - callback: Callback - ): Promise { - // This method is overridden as the auto-generated one does not include the - // watcherId parameter, as a result of the peculiarity of the call, which - // does not assume the id to be in parameters, but as a top level field. - return new Promise((resolve, reject) => { - // Prepare the request to the Juju API. - const req = { - type: "AllWatcher", - request: "Stop", - version: this.version, - id: watcherId, - }; - const handler = createAsyncHandler(callback, resolve, reject); - // Send the request to the server. - this._transport.write(req, handler.resolve, handler.reject); - }); - }; - - return cls; -} - -/** - Decorate the Client facade class. - - @param cls The auto-generated class. - @returns The decorated class. -*/ -function wrapClient(cls: any) { - /** - Watch changes in the current model, and call the provided callback every - time changes arrive. - - This method requires the AllWatcher facade to be loaded and available to the - client. - - @param callback Called every time changes arrive from Juju, the - callback receives an error and a the changes. If there are no errors, - changes are provided as an object like the following: - { - deltas: []anything - } - @returns A handle that can be used to stop watching, via its stop - method which can be provided a callback receiving an error. - */ - cls.prototype.watch = function (callback: Callback): object | undefined { - // Check that the AllWatcher facade is loaded, as we will use it. - const allWatcher = this._info.getFacade("allWatcher"); - if (!allWatcher) { - callback(new Error("watch requires the allWatcher facade to be loaded")); - return; - } - let watcherId: any; - // Define a function to repeatedly ask for next changes. - const next = (callback: Callback) => { - if (!watcherId) { - return; - } - allWatcher.next(watcherId, (error: CallbackError, result: any) => { - callback(error, result); - next(callback); - }); - }; - // Start watching. - this.watchAll((error: CallbackError, result: any) => { - if (error) { - callback(error); - return; - } - watcherId = result.watcherId; - next(callback); - }); - // Return the handle allowing for stopping the watcher. - return { - stop: (callback: Callback) => { - if (watcherId === undefined) { - callback(new Error("watcher is not running")); - return; - } - allWatcher.stop(watcherId, callback); - watcherId = undefined; - }, - }; - }; - - return cls; -} - -/** - Decorate the Controller facade class. - - @param cls The auto-generated class. - @returns The decorated class. -*/ -function wrapController(cls: any) { - /** - Watch changes in the all models on this controller, and call the provided - callback every time changes arrive. - - This method requires the AllModelWatcher facade to be loaded and available - to the client. - - @param callback Called every time changes arrive from Juju, the - callback receives an error and a the changes. If there are no errors, - changes are provided as an object like the following: - { - deltas: []anything - } - @returns A handle that can be used to stop watching, via its stop - method which can be provided a callback receiving an error. - */ - cls.prototype.watch = function (callback: Callback): object | undefined { - // Check that the AllModelWatcher facade is loaded, as we will use it. - const allModelWatcher: any = this._info.getFacade("allModelWatcher"); - if (!allModelWatcher) { - callback( - new Error("watch requires the allModelWatcher facade to be loaded") - ); - return; - } - let watcherId: any; - // Define a function to repeatedly ask for next changes. - const next = (callback: Callback) => { - if (!watcherId) { - return; - } - allModelWatcher.next(watcherId, (error: CallbackError, result: any) => { - callback(error, result); - next(callback); - }); - }; - // Start watching. - this.watchAllModels((error: CallbackError, result: any) => { - if (error) { - callback(error); - return; - } - watcherId = result.watcherId; - next(callback); - }); - // Return the handle allowing for stopping the watcher. - return { - stop: (callback: Callback) => { - if (watcherId === undefined) { - callback(new Error("watcher is not running")); - return; - } - allModelWatcher.stop(watcherId, callback); - watcherId = undefined; - }, - }; - }; - - return cls; -} - -type StopFunction = () => void; -/** - Ping repeatedly using the Pinger facade. - - @param PingerFacade - An instance of the Pinger facade. - @param interval - How often would you like to ping? (ms). - @param callback - The callback that gets called after each ping. - @returns A function to call to stop the pinger. -*/ -export function pingForever( - PingerFacade: PingerV1, - interval: number, - callback: Callback -): StopFunction { - const timer = setInterval(async () => { - const resp = await PingerFacade.ping(null); - if (callback) { - callback(resp); - } - }, interval); - return () => { - clearInterval(timer); - }; -} - -export { - wrapAdmin, - wrapAllModelWatcher, - wrapAllWatcher, - wrapClient, - wrapController, -}; diff --git a/api/tests/helpers.ts b/api/tests/helpers.ts index a8256a8ac..2427c5b81 100644 --- a/api/tests/helpers.ts +++ b/api/tests/helpers.ts @@ -259,7 +259,7 @@ export class MockWebSocket { @returns The mock bakery instance. */ function makeBakery(succeeding: boolean) { - const fakeMacaroon = btoa(JSON.stringify("fake macaroon")); + const fakeMacaroon = btoa(JSON.stringify({ fake: "macaroon" })); const mockBakery: unknown = { discharge: ( _macaroon: Macaroon, diff --git a/api/tests/test-client.ts b/api/tests/test-client.ts index c6d0b94f6..23c5673f4 100644 --- a/api/tests/test-client.ts +++ b/api/tests/test-client.ts @@ -376,7 +376,7 @@ describe("connect", () => { done(); }); // Reply to the login request with a discharge required response. - ws.reply({ response: { "discharge-required": "macaroon" } }); + ws.reply({ response: { "discharge-required": { fake: "macaroon" } } }); }); // Open the WebSocket connection. ws.open(); @@ -398,7 +398,7 @@ describe("connect", () => { done(); }); // Reply to the login request with a discharge required response. - ws.reply({ response: { "discharge-required": "macaroon" } }); + ws.reply({ response: { "discharge-required": { fake: "macaroon" } } }); }); ws.open(); }); @@ -411,7 +411,7 @@ describe("connect", () => { "auth-tag": "", "client-version": CLIENT_VERSION, credentials: "", - macaroons: ["fake macaroon"], + macaroons: [{ fake: "macaroon" }], nonce: "", "user-data": "", }, @@ -441,7 +441,7 @@ describe("connect", () => { done(); }); // Reply to the login request with a discharge required response. - ws.reply({ response: { "discharge-required": "macaroon" } }); + ws.reply({ response: { "discharge-required": { fake: "macaroon" } } }); }); // Open the WebSocket connection. ws.open(); @@ -464,7 +464,7 @@ describe("connect", () => { done(); }); // Reply to the login request with a discharge required response. - ws.reply({ response: { "discharge-required": "macaroon" } }); + ws.reply({ response: { "discharge-required": { fake: "macaroon" } } }); }); ws.open(); }); @@ -477,7 +477,7 @@ describe("connect", () => { "auth-tag": "", "client-version": CLIENT_VERSION, credentials: "", - macaroons: ["fake macaroon"], + macaroons: [{ fake: "macaroon" }], nonce: "", "user-data": "", }, @@ -500,7 +500,7 @@ describe("connect", () => { ]) ); // Reply to the login request with a discharge required response. - ws.reply({ response: { "discharge-required": "macaroon" } }); + ws.reply({ response: { "discharge-required": { fake: "macaroon" } } }); }); // Open the WebSocket connection. ws.open(); @@ -529,7 +529,7 @@ describe("connect", () => { ]) ); // Reply to the login request with a discharge required response. - ws.reply({ response: { "discharge-required": "macaroon" } }); + ws.reply({ response: { "discharge-required": { fake: "macaroon" } } }); }); ws.open(); }); diff --git a/api/tests/test-wrappers.ts b/api/tests/test-wrappers.ts deleted file mode 100644 index 8342aff93..000000000 --- a/api/tests/test-wrappers.ts +++ /dev/null @@ -1,600 +0,0 @@ -// Copyright 2018 Canonical Ltd. -// Licensed under the LGPLv3, see LICENSE.txt file for details. - -"use strict"; - -import { BaseFacade, makeConnection, requestEqual } from "./helpers"; -import { - wrapAllModelWatcher, - wrapAllWatcher, - wrapClient, - wrapController, -} from "../helpers"; -import { Callback, CallbackError } from "../../generator/interfaces"; - -describe("wrapAllModelWatcher", () => { - class AllModelWatcherV1 extends BaseFacade { - static NAME = "AllModelWatcher"; - static VERSION = 1; - - next( - _id: number, - _callback: (error: CallbackError, result?: Response) => void - ) {} - stop( - _id: number, - _callback: (error: CallbackError, result?: Response) => void - ) {} - } - const options = { - closeCallback: jest.fn(), - facades: [wrapAllModelWatcher(AllModelWatcherV1)], - }; - - it("next success", (done) => { - makeConnection(options, (conn, ws) => { - const allModelWatcher = conn?.info.getFacade?.( - "allModelWatcher" - ) as AllModelWatcherV1; - const watcherId = 42; - allModelWatcher?.next(watcherId, (_error, result) => { - requestEqual(ws.lastRequest, { - type: "AllModelWatcher", - request: "Next", - version: 1, - }); - expect(ws.lastRequest?.["id"]).toBe(watcherId); - expect(result).toStrictEqual({ - deltas: [ - ["model", "change", { name: "default" }], - ["machine", "change", { name: "machine2" }], - ], - }); - }); - // Reply to the next request. - ws.reply({ - response: { - deltas: [ - ["model", "change", { name: "default" }], - ["machine", "change", { name: "machine2" }], - ], - }, - }); - done(); - }); - }); - - it("next failure", (done) => { - makeConnection(options, (conn, ws) => { - const allModelWatcher = conn?.info.getFacade?.( - "allModelWatcher" - ) as AllModelWatcherV1; - const watcherId = 42; - allModelWatcher?.next(watcherId, (err) => { - expect(err).toStrictEqual(new Error("bad wolf")); - }); - // Reply to the next request. - ws.reply({ error: "bad wolf" }); - done(); - }); - }); - - it("stop success", (done) => { - makeConnection(options, (conn, ws) => { - const allModelWatcher = conn?.info.getFacade?.( - "allModelWatcher" - ) as AllModelWatcherV1; - const watcherId = 42; - allModelWatcher?.stop(watcherId, (_error, result) => { - requestEqual(ws.lastRequest, { - type: "AllModelWatcher", - request: "Stop", - version: 1, - }); - expect(ws.lastRequest?.["id"]).toBe(watcherId); - expect(result).toStrictEqual({}); - }); - // Reply to the next request. - ws.reply({ response: {} }); - done(); - }); - }); - - it("stop failure", (done) => { - makeConnection(options, (conn, ws) => { - const allModelWatcher = conn?.info.getFacade?.( - "allModelWatcher" - ) as AllModelWatcherV1; - const watcherId = 42; - allModelWatcher?.stop(watcherId, (err) => { - expect(err).toStrictEqual(new Error("bad wolf")); - }); - // Reply to the next request. - ws.reply({ error: "bad wolf" }); - done(); - }); - }); -}); - -describe("wrapAllWatcher", () => { - class AllWatcherV0 extends BaseFacade { - static NAME = "AllWatcher"; - static VERSION = 0; - - next( - _id: number, - _callback: (error: CallbackError, result?: Response) => void - ) {} - stop( - _id: number, - _callback: (error: CallbackError, result?: Response) => void - ) {} - } - const options = { - closeCallback: jest.fn(), - facades: [wrapAllWatcher(AllWatcherV0)], - }; - - it("next success", (done) => { - makeConnection(options, (conn, ws) => { - const allWatcher = conn?.info.getFacade?.("allWatcher") as AllWatcherV0; - const watcherId = 42; - allWatcher?.next(watcherId, (_error, result) => { - requestEqual(ws.lastRequest, { - type: "AllWatcher", - request: "Next", - version: 0, - }); - expect(ws.lastRequest?.["id"]).toBe(watcherId); - expect(result).toStrictEqual({ - deltas: [ - ["app", "remove", { name: "app1" }], - ["machine", "change", { name: "machine2" }], - ], - }); - }); - // Reply to the next request. - ws.reply({ - response: { - deltas: [ - ["app", "remove", { name: "app1" }], - ["machine", "change", { name: "machine2" }], - ], - }, - }); - done(); - }); - }); - - it("next failure", (done) => { - makeConnection(options, (conn, ws) => { - const allWatcher = conn?.info.getFacade?.("allWatcher") as AllWatcherV0; - const watcherId = 42; - allWatcher?.next(watcherId, (err) => { - expect(err).toStrictEqual(new Error("bad wolf")); - }); - // Reply to the next request. - ws.reply({ error: "bad wolf" }); - done(); - }); - }); - - it("stop success", (done) => { - makeConnection(options, (conn, ws) => { - const allWatcher = conn?.info.getFacade?.("allWatcher") as AllWatcherV0; - const watcherId = 42; - allWatcher.stop(watcherId, (_error, result) => { - requestEqual(ws.lastRequest, { - type: "AllWatcher", - request: "Stop", - version: 0, - }); - expect(ws.lastRequest?.["id"]).toBe(watcherId); - expect(result).toStrictEqual({}); - }); - // Reply to the next request. - ws.reply({ response: {} }); - done(); - }); - }); - - it("stop failure", (done) => { - makeConnection(options, (conn, ws) => { - const allWatcher = conn?.info.getFacade?.("allWatcher") as AllWatcherV0; - const watcherId = 42; - allWatcher.stop(watcherId, (err) => { - expect(err).toStrictEqual(new Error("bad wolf")); - }); - // Reply to the next request. - ws.reply({ error: "bad wolf" }); - done(); - }); - }); -}); - -describe("wrapClient", () => { - it("watch success", (done) => { - class ClientV3 extends BaseFacade { - static NAME = "Client"; - static VERSION = 3; - - watchAll(callback: Callback>) { - callback(null, { watcherId: 47 }); - } - - watch(callback: Callback>) { - callback(null, { watcherId: 47 }); - } - } - class AllWatcherV0 extends BaseFacade { - static NAME = "AllWatcher"; - static VERSION = 0; - } - const options = { - closeCallback: jest.fn(), - facades: [wrapClient(ClientV3), wrapAllWatcher(AllWatcherV0)], - }; - makeConnection(options, (conn, ws) => { - const client = conn?.info.getFacade?.("client") as ClientV3; - let callCount = 0; - client?.watch((_error, result) => { - callCount += 1; - switch (callCount) { - case 1: - expect(result).toStrictEqual({ - deltas: [ - ["app", "remove", { name: "app1" }], - ["machine", "change", { name: "machine2" }], - ], - }); - break; - case 2: - expect(result).toStrictEqual({ - deltas: [["app", "change", { name: "app2" }]], - }); - break; - } - }); - // Reply to next requests. - ws.reply({ - response: { - deltas: [ - ["app", "remove", { name: "app1" }], - ["machine", "change", { name: "machine2" }], - ], - }, - }); - ws.reply({ - response: { - deltas: [["app", "change", { name: "app2" }]], - }, - }); - done(); - }); - }); - - it("watch failure allWatcher not found", (done) => { - class ClientV3 extends BaseFacade { - static NAME = "Client"; - static VERSION = 3; - - watch(_callback: (result: Response | CallbackError) => void) {} - } - const options = { - closeCallback: jest.fn(), - facades: [wrapClient(ClientV3)], - }; - makeConnection(options, (conn, ws) => { - const client = conn?.info.getFacade?.("client") as ClientV3; - client.watch((err) => { - expect(err).toStrictEqual( - new Error("watch requires the allWatcher facade to be loaded") - ); - // Only the login request has been made, no other requests. - expect(ws.requests.length).toBe(1); - }); - done(); - }); - }); - - it("watch failure on initial watch request", (done) => { - class ClientV3 extends BaseFacade { - static NAME = "Client"; - static VERSION = 3; - watchAll(callback: Callback>) { - callback(new Error("bad wolf"), {}); - } - watch(callback: Callback>) { - callback(new Error("bad wolf"), {}); - } - } - class AllWatcherV0 extends BaseFacade { - static NAME = "AllWatcher"; - static VERSION = 0; - } - const options = { - closeCallback: jest.fn(), - facades: [wrapClient(ClientV3), wrapAllWatcher(AllWatcherV0)], - }; - makeConnection(options, (conn, _ws) => { - const client = conn?.info.getFacade?.("client") as ClientV3; - client.watch((err) => { - expect(err).toStrictEqual(new Error("bad wolf")); - }); - done(); - }); - }); - - it("watch failure on next request", (done) => { - class ClientV3 extends BaseFacade { - static NAME = "Client"; - static VERSION = 3; - watchAll(callback: Callback>) { - callback(null, { watcherId: 47 }); - } - watch(callback: Callback>) { - callback(null, { watcherId: 47 }); - } - } - class AllWatcherV0 extends BaseFacade { - static NAME = "AllWatcher"; - static VERSION = 0; - } - const options = { - closeCallback: jest.fn(), - facades: [wrapClient(ClientV3), wrapAllWatcher(AllWatcherV0)], - }; - makeConnection(options, (conn, ws) => { - const client = conn?.info.getFacade?.("client") as ClientV3; - client.watch((err) => { - expect(err).toStrictEqual(new Error("bad wolf")); - }); - // Reply to the next request. - ws.reply({ error: "bad wolf" }); - done(); - }); - }); - - it("addMachine success", (done) => { - let gotArgs: unknown = null; - class ClientV3 extends BaseFacade { - static NAME = "Client"; - static VERSION = 3; - addMachines( - args: unknown, - callback: Callback<{ machines: { machine: number }[] }> - ) { - gotArgs = args; - callback(null, { machines: [{ machine: 42 }] }); - } - } - const options = { - closeCallback: jest.fn(), - facades: [wrapClient(ClientV3)], - }; - makeConnection(options, (conn, _ws) => { - const client = conn?.info.getFacade?.("client") as ClientV3; - client.addMachines( - { - arch: "amd64", - constraints: { cores: 8 }, - jobs: ["job1", "job2"], - parentId: 2, - series: "bionic", - }, - (err, result) => { - expect(err).toBe(null); - expect(result).toStrictEqual({ machines: [{ machine: 42 }] }); - expect(gotArgs).toStrictEqual({ - arch: "amd64", - constraints: { cores: 8 }, - jobs: ["job1", "job2"], - parentId: 2, - series: "bionic", - }); - } - ); - done(); - }); - }); - - it("addMachine success without jobs", (done) => { - let gotArgs: unknown | null = null; - class ClientV3 extends BaseFacade { - static NAME = "Client"; - static VERSION = 3; - addMachines( - args: unknown, - callback: Callback<{ machines: { machine: number }[] }> - ) { - gotArgs = args; - callback(null, { machines: [{ machine: 42 }] }); - } - } - const options = { - closeCallback: jest.fn(), - facades: [wrapClient(ClientV3)], - }; - makeConnection(options, (conn, _ws) => { - const client = conn?.info.getFacade?.("client") as ClientV3; - client.addMachines({ series: "cosmic" }, (_err, _result) => { - expect(gotArgs).toStrictEqual({ - series: "cosmic", - }); - }); - done(); - }); - }); - - it("addMachine failure", (done) => { - class ClientV3 extends BaseFacade { - static NAME = "Client"; - static VERSION = 3; - addMachines(args: unknown, callback: Callback>) { - callback(new Error("bad wolf"), {}); - } - } - const options = { - closeCallback: jest.fn(), - facades: [wrapClient(ClientV3)], - }; - makeConnection(options, (conn, _ws) => { - const client = conn?.info.getFacade?.("client") as ClientV3; - client.addMachines({ series: "bionic" }, (err) => { - expect(err).toStrictEqual(new Error("bad wolf")); - }); - done(); - }); - }); -}); - -describe("wrapController", () => { - it("watch success", (done) => { - class ControllerV4 extends BaseFacade { - static NAME = "Controller"; - static VERSION = 4; - watchAllModels(callback: Callback>) { - callback(null, { watcherId: 47 }); - } - watch(callback: Callback>) { - callback(null, { watcherId: 47 }); - } - } - class AllModelWatcherV1 extends BaseFacade { - static NAME = "AllModelWatcher"; - static VERSION = 1; - } - const options = { - closeCallback: jest.fn(), - facades: [ - wrapController(ControllerV4), - wrapAllModelWatcher(AllModelWatcherV1), - ], - }; - makeConnection(options, (conn, ws) => { - const controller = conn?.info.getFacade?.("controller") as ControllerV4; - let callCount = 0; - controller?.watch((_error, result) => { - callCount += 1; - switch (callCount) { - case 1: - expect(result).toStrictEqual({ - deltas: [ - ["model", "change", { name: "default" }], - ["machine", "change", { name: "machine2" }], - ], - }); - break; - case 2: - expect(result).toStrictEqual({ - deltas: [["app", "change", { name: "app2" }]], - }); - break; - } - }); - // Reply to next requests. - ws.reply({ - response: { - deltas: [ - ["model", "change", { name: "default" }], - ["machine", "change", { name: "machine2" }], - ], - }, - }); - ws.reply({ - response: { - deltas: [["app", "change", { name: "app2" }]], - }, - }); - done(); - }); - }); - - it("watch failure allWatcher not found", (done) => { - class ControllerV4 extends BaseFacade { - static NAME = "Controller"; - static VERSION = 4; - - watch(_callback: Callback) {} - } - const options = { - closeCallback: jest.fn(), - facades: [wrapController(ControllerV4)], - }; - makeConnection(options, (conn, ws) => { - const controller = conn?.info.getFacade?.("controller") as ControllerV4; - controller.watch((err) => { - expect(err).toStrictEqual( - new Error("watch requires the allModelWatcher facade to be loaded") - ); - // Only the login request has been made, no other requests. - expect(ws.requests.length).toBe(1); - }); - done(); - }); - }); - - it("watch failure on initial watch request", (done) => { - class ControllerV4 extends BaseFacade { - static NAME = "Controller"; - static VERSION = 4; - watchAllModels(callback: Callback>) { - callback(new Error("bad wolf"), {}); - } - watch(callback: Callback>) { - callback(new Error("bad wolf"), {}); - } - } - class AllModelWatcherV1 extends BaseFacade { - static NAME = "AllModelWatcher"; - static VERSION = 1; - } - const options = { - closeCallback: jest.fn(), - facades: [ - wrapController(ControllerV4), - wrapAllModelWatcher(AllModelWatcherV1), - ], - }; - makeConnection(options, (conn, _ws) => { - const controller = conn?.info.getFacade?.("controller") as ControllerV4; - controller.watch((err) => { - expect(err).toStrictEqual(new Error("bad wolf")); - }); - done(); - }); - }); - - it("watch failure on next request", (done) => { - class ControllerV5 extends BaseFacade { - static NAME = "Controller"; - static VERSION = 5; - watchAllModels(callback: Callback>) { - callback(null, { watcherId: 47 }); - } - watch(callback: Callback>) { - callback(null, { watcherId: 47 }); - } - } - class AllModelWatcherV1 extends BaseFacade { - static NAME = "AllModelWatcher"; - static VERSION = 1; - } - const options = { - closeCallback: jest.fn(), - facades: [ - wrapController(ControllerV5), - wrapAllModelWatcher(AllModelWatcherV1), - ], - }; - makeConnection(options, (conn, ws) => { - const controller = conn?.info.getFacade?.("controller") as ControllerV5; - controller.watch((err) => { - expect(err).toStrictEqual(new Error("bad wolf")); - }); - // Reply to the next request. - ws.reply({ error: "bad wolf" }); - done(); - }); - }); -}); diff --git a/api/utils.ts b/api/utils.ts index a78a756c4..7508ae36e 100644 --- a/api/utils.ts +++ b/api/utils.ts @@ -25,11 +25,11 @@ export function autoBind(obj: { [k: string]: any }): void { Create an async handler which will either return a value to a supplied callback, or call the appropriate method on the promise resolve/reject. - @param {Function} [callback] The optional callback. - @param {Function} [resolve] The optional promise resolve function. - @param {Function} [reject] The optional promise reject function. - @param {Function} [transform] The optional response transform function. - @return {Function} The returned function takes two arguments (err, value). + @param [callback] The optional callback. + @param [resolve] The optional promise resolve function. + @param [reject] The optional promise reject function. + @param [transform] The optional response transform function. + @return The returned function takes two arguments (err, value). If the the callback is a function the two arguments will be passed through to the callback in the same order. If no callback is supplied, the promise resolve or reject method will be called depending on the existence of an diff --git a/examples/ping.js b/examples/ping.js deleted file mode 100644 index 3e3c07b14..000000000 --- a/examples/ping.js +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2020 Canonical Ltd. -// Licensed under the LGPLv3, see LICENSE.txt file for details. - -// Allow connecting endpoints using self-signed certs. -process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; - -import websocket from "websocket"; -import * as jujulib from "../api/client.js"; -import { pingForever } from "../api/helpers.js"; - -import PingerV1 from "../api/facades/pinger/PingerV1.js"; - -const url = - "wss://10.223.241.216:17070/model/7236b7b8-5458-4d3e-8a9a-1c8f1a0046b1/api"; - -const facades = [PingerV1]; -const options = { - debug: true, - facades: facades, - wsclass: websocket.w3cwebsocket, -}; - -async function ping() { - try { - const juju = await jujulib.connect(url, options); - const conn = await juju.login({ - username: "admin", - password: "ca83f25a8fd8e162641b60f7a5fd1049", - }); - const stopFn = pingForever(conn.facades.pinger, 1000, (resp) => { - if (resp.error) { - console.log("cannot ping:", resp.error); - process.exit(1); - } - console.log("pong"); - }); - - setTimeout(stopFn, 5000); - } catch (error) { - console.log("cannot connect:", error); - process.exit(1); - } -} - -ping();