Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,14 +188,12 @@ export type ILoginCredentials =
* Common args for POST, GET, PUT, DELETE requests
* @param endpoint The API endpoint (including version) e.g. `chat.update`
* @param data Payload for POST request to endpoint
* @param auth Require auth headers for endpoint, default true
* @param ignore Allows certain matching error messages to not count as errors
*/
export interface IAPIRequest {
(
endpoint: string,
data?: any,
auth?: boolean,
ignore?: RegExp,
options?: any,
apiVersion?: string
Expand Down
26 changes: 13 additions & 13 deletions lib/api/RocketChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,33 +28,33 @@ export default class ApiRocketChat extends ApiBase {
online: (fields: any = userFields) => this.get('users.list', { fields, query: { 'status': { $ne: 'offline' } } }).then((r: any) => r.users),
onlineNames: () => this.get('users.list', { fields: { 'username': 1 }, query: { 'status': { $ne: 'offline' } } }).then((r: any) => r.users.map((u: IUserAPI) => u.username)),
onlineIds: () => this.get('users.list', { fields: { '_id': 1 }, query: { 'status': { $ne: 'offline' } } }).then((r: any) => r.users.map((u: IUserAPI) => u._id)),
info: async (username: string): Promise<IUserAPI> => (await this.get('users.info', { username }, true)).user
info: async (username: string): Promise<IUserAPI> => (await this.get('users.info', { username })).user
}
}

get rooms (): any {
return {
info: ({ rid }: any) => this.get('rooms.info', { rid }, true)
info: ({ rid }: any) => this.get('rooms.info', { rid })
}
}

// editMessage(message: IMessage) chat.update
joinRoom ({ rid }: any) { return this.post('channels.join', { roomId: rid }, true) }
joinRoom ({ rid }: any) { return this.post('channels.join', { roomId: rid }) }

async info () { return (await this.get('info', {}, this.loggedIn())).info }
async info () { return (await this.get('info', {})).info }
/**
* Send a prepared message object (with pre-defined room ID).
* Usually prepared and called by sendMessageByRoomId or sendMessageByRoom.
*/
async sendMessage (message: IMessage | string, rid: string): Promise<IMessageReceipt> { return (await this.post('chat.sendMessage', { message: this.prepareMessage(message, rid) }, true)).message }
getRoomIdByNameOrId (name: string): Promise<RID> { return this.get('chat.getRoomIdByNameOrId', { name }, true) }
async sendMessage (message: IMessage | string, rid: string): Promise<IMessageReceipt> { return (await this.post('chat.sendMessage', { message: this.prepareMessage(message, rid) })).message }
getRoomIdByNameOrId (name: string): Promise<RID> { return this.get('chat.getRoomIdByNameOrId', { name }) }
getRoomNameById (rid: RID): Promise<string> { return this.getRoomName(rid) }
async getRoomName (rid: string): Promise<string> {
const room = await this.get('chat.getRoomNameById', { rid }, true)
const room = await this.get('chat.getRoomNameById', { rid })
return room.name
}
getRoomId (name: string) { return this.get('chat.find', { name }, true) }
async createDirectMessage (username: string) { return (await this.post('im.create', { username }, true)).room }
getRoomId (name: string) { return this.get('chat.find', { name }) }
async createDirectMessage (username: string) { return (await this.post('im.create', { username })).room }

/**
* Edit an existing message, replacing any attributes with those provided.
Expand All @@ -68,15 +68,15 @@ export default class ApiRocketChat extends ApiBase {
* @param emoji Accepts string like `:thumbsup:` to add 👍 reaction
* @param messageId ID for a previously sent message
*/
setReaction (emoji: string, messageId: string) { return this.post('chat.react', { emoji, messageId }, true) }
setReaction (emoji: string, messageId: string) { return this.post('chat.react', { emoji, messageId }) }

// TODO fix this methods

async loadHistory (rid: string, lastUpdate: Date): Promise<{
updated: IMessage[],
deleted: IMessage[]
}> {
return (await this.get('chat.syncMessages', { roomId: rid, lastUpdate: lastUpdate.toISOString() }, true)).result
return (await this.get('chat.syncMessages', { roomId: rid, lastUpdate: lastUpdate.toISOString() })).result
}
/** Exit a room the bot has joined */
leaveRoom (rid: string): Promise<RID> {
Expand All @@ -85,11 +85,11 @@ export default class ApiRocketChat extends ApiBase {

/** Get information about a public group */
async channelInfo (query: { roomName?: string, roomId?: string }) {
return (await this.get('channels.info', query, true)).channel as Promise<IChannelAPI>
return (await this.get('channels.info', query)).channel as Promise<IChannelAPI>
}

/** Get information about a private group */
async privateInfo (query: { roomName?: string, roomId?: string }) {
return (await this.get('groups.info', query, true)).group as Promise<IGroupAPI>
return (await this.get('groups.info', query)).group as Promise<IGroupAPI>
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,28 @@ import { anonymousApiWithFakeClient, anonymousApiRocketChatWithFakeClient } from

const infoResponse = () => ({ status: 200, data: { info: { version: '6.0.0' } } })

describe('Api auth guard', () => {
it('reports not logged in with no login', () => {
describe('Api with no Current login', () => {
it('reports no Current login', () => {
const { api } = anonymousApiWithFakeClient()

expect(api.loggedIn()).toBe(false)
})

it('refuses an authenticated request with no login', async () => {
it('sends the Endpoint to the REST client with no Current login', async () => {
const { api, restClient } = anonymousApiWithFakeClient()

await expect(api.get('me', {})).rejects.toThrow(/requires a login/)
expect(restClient.requests).toHaveLength(0)
})

it('allows an unauthenticated request with no login', async () => {
const { api, restClient } = anonymousApiWithFakeClient()

const pending = api.get('settings.public', {}, false)
restClient.lastRequest().resolve({ status: 200, data: { settings: [] } })
const pending = api.post('users.forgotPassword', { email: 'user@example.com' })
restClient.lastRequest().resolve({ status: 200, data: { success: true } })

await expect(pending).resolves.toEqual({ settings: [] })
await expect(pending).resolves.toEqual({ success: true })
expect(restClient.requests).toHaveLength(1)
expect(restClient.lastRequest()).toMatchObject({
endpoint: 'users.forgotPassword',
data: { email: 'user@example.com' }
})
})

it('logs in with no prior login', async () => {
it('sets a Current login from a login with none held', async () => {
const { api, restClient } = anonymousApiWithFakeClient()

const pending = api.login({ username: 'user', password: 'pass' })
Expand All @@ -36,7 +34,7 @@ describe('Api auth guard', () => {
expect(api.loggedIn()).toBe(true)
})

it('allows info() with no login', async () => {
it('sends info() with no Current login', async () => {
const { api, restClient } = anonymousApiRocketChatWithFakeClient()

const pending = api.info()
Expand All @@ -45,7 +43,7 @@ describe('Api auth guard', () => {
await expect(pending).resolves.toEqual({ version: '6.0.0' })
})

it('sends info() authenticated once logged in', async () => {
it('sends info() with the auth headers once a Current login is held', async () => {
const { api, restClient } = anonymousApiRocketChatWithFakeClient()

const login = api.login({ username: 'user', password: 'pass' })
Expand Down
12 changes: 6 additions & 6 deletions lib/api/__tests__/api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ describe('api', () => {
const { api, restClient } = anonymousApiWithFakeClient()
restClient.enqueueReply(emptySuccess())

await api.post('login', { username: 'user' }, false)
await api.post('login', { username: 'user' })

expect(restClient.lastRequest()).toMatchObject({
method: 'POST',
Expand Down Expand Up @@ -125,10 +125,10 @@ describe('api', () => {
const { api, restClient } = await loggedInApiWithFakeClient()
restClient.enqueueReply(emptySuccess(), emptySuccess(), emptySuccess(), emptySuccess())

await api.get('chat.getMessage', {}, true, undefined, {}, 'v2')
await api.post('chat.postMessage', {}, true, undefined, {}, 'v2')
await api.put('chat.update', {}, true, undefined, {}, 'v2')
await api.del('chat.delete', {}, true, undefined, {}, 'v2')
await api.get('chat.getMessage', {}, undefined, {}, 'v2')
await api.post('chat.postMessage', {}, undefined, {}, 'v2')
await api.put('chat.update', {}, undefined, {}, 'v2')
await api.del('chat.delete', {}, undefined, {}, 'v2')

expect(restClient.requests.map((request) => request.apiVersion)).toEqual([
'v2', 'v2', 'v2', 'v2'
Expand Down Expand Up @@ -169,7 +169,7 @@ describe('api', () => {
const { api, restClient } = await loggedInApiWithFakeClient()
restClient.enqueueReply({ status: 400, data: { error: 'nope' } })

await expect(api.get('me', {}, true, /400/)).resolves.toEqual({ error: 'nope' })
await expect(api.get('me', {}, /400/)).resolves.toEqual({ error: 'nope' })
})

it('throws when the restClient answers nothing at all', async () => {
Expand Down
8 changes: 4 additions & 4 deletions lib/api/__tests__/client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('REST client', () => {
})

it('addresses the api version the caller asked for', async () => {
await api.get('rooms.info', {}, true, undefined, {}, 'v2')
await api.get('rooms.info', {}, undefined, {}, 'v2')

expect(lastFetchCall().url).toBe('http://localhost:3000/api/v2/rooms.info?')
})
Expand Down Expand Up @@ -99,21 +99,21 @@ describe('REST client', () => {
it('drops the auth headers on a logout and keeps resolving the custom ones', async () => {
jest.replaceProperty(settings, 'customHeaders', { 'X-Custom': 'first' })
await api.logout()
await api.get('settings.public', {}, false)
await api.get('settings.public', {})

expect(lastFetchCall().init.headers).toEqual({
'Content-Type': 'application/json',
'X-Custom': 'first'
})

jest.replaceProperty(settings, 'customHeaders', { 'X-Custom': 'second' })
await api.get('settings.public', {}, false)
await api.get('settings.public', {})

expect(lastFetchCall().init.headers).toMatchObject({ 'X-Custom': 'second' })
})

it('sends only the headers the caller passed as options', async () => {
await api.get('me', {}, true, undefined, { customHeaders: { 'X-Only': 'this' } })
await api.get('me', {}, undefined, { customHeaders: { 'X-Only': 'this' } })

expect(lastFetchCall().init.headers).toEqual({ 'X-Only': 'this' })
})
Expand Down
82 changes: 6 additions & 76 deletions lib/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,70 +13,6 @@ import {
import { SDKEventEmitter } from '../emitter'
import * as settings from '../settings';

/** Check for existing login */
// export function loggedIn () {
// return (currentLogin !== null)
// }

/**
* Prepend protocol (or put back if removed from env settings for driver)
* Hard code endpoint prefix, because all syntax depends on this version
*/
// export const url = `${(host.indexOf('http') === -1) ? host.replace(/^(\/\/)?/, 'http://') : host}/api/v1/`

/** Populate auth headers (from response data on login) */
// export function setAuth (authData: {authToken: string, userId: string}) {
// client.defaults.headers.common['X-Auth-Token'] = authData.authToken
// client.defaults.headers.common['X-User-Id'] = authData.userId
// }

// /** Clear headers so they can't be used without logging in again */
// export function clearHeaders () {
// delete client.defaults.headers.common['X-Auth-Token']
// delete client.defaults.headers.common['X-User-Id']
// }

// /**
// * Login a user for further API calls
// * Result should come back with a token, to authorise following requests.
// * Use env default credentials, unless overridden by login arguments.
// */
// export async function login (user: ICredentialsAPI = { username, password }) {
// this.logger.info(`[API] Logging in ${user.username}`)
// if (currentLogin !== null) {
// this.logger.debug(`[API] Already logged in`)
// if (currentLogin.username === user.username) return currentLogin.result
// else await logout()
// }
// const result = (await this.post('login', user, false) as ILoginResultAPI)
// if (result && result.data && result.data.authToken) {
// currentLogin = {
// result: result, // keep to return if login requested again for same user
// username: user.username, // keep to compare with following login attempt
// authToken: result.data.authToken,
// userId: result.data.userId
// }
// setAuth(currentLogin)
// this.logger.info(`[API] Logged in ID ${currentLogin.userId}`)
// return result
// } else {
// throw new Error(`[API] Login failed for ${user.username}`)
// }
// }

// /** Logout a user at end of API calls */
// export function logout () {
// if (currentLogin === null) {
// this.logger.debug(`[API] Already logged out`)
// return Promise.resolve()
// }
// this.logger.info(`[API] Logging out ${ currentLogin.username }`)
// return this.get('logout', null, true).then(() => {
// clearHeaders()
// currentLogin = null
// })
// }

export interface IClient {
host: string
headers: any
Expand Down Expand Up @@ -219,24 +155,18 @@ export default class Api extends SDKEventEmitter {
* @param method Request method GET | POST | PUT | DEL
* @param endpoint The API endpoint (including version) e.g. `chat.update`
* @param data Payload for POST request to endpoint
* @param auth Require auth headers for endpoint, default true
* @param ignore Allows certain matching error messages to not count as errors
*/
request = async (
method: 'POST' | 'GET' | 'PUT' | 'DELETE',
endpoint: string,
data: any = {},
auth: boolean = true,
ignore?: RegExp,
options?: any,
apiVersion: string = 'v1'
) => {
this.logger?.debug(`[API] ${ method } ${ endpoint }: ${ JSON.stringify(data) }`)
try {
if (auth && !this.loggedIn()) {
throw new Error(`API ${ method } ${ endpoint } requires a login`)
}

const { signal } = this.controller;
options = { ...options, signal };

Expand All @@ -259,16 +189,16 @@ export default class Api extends SDKEventEmitter {
}
}
/** Do a POST request to an API endpoint. */
post: IAPIRequest = (endpoint, data, auth, ignore, options = {}, apiVersion) => this.request('POST', endpoint, data, auth, ignore, options, apiVersion)
post: IAPIRequest = (endpoint, data, ignore, options = {}, apiVersion) => this.request('POST', endpoint, data, ignore, options, apiVersion)

/** Do a GET request to an API endpoint. */
get: IAPIRequest = (endpoint, data, auth, ignore, options = {}, apiVersion) => this.request('GET', endpoint, data, auth, ignore, options, apiVersion)
get: IAPIRequest = (endpoint, data, ignore, options = {}, apiVersion) => this.request('GET', endpoint, data, ignore, options, apiVersion)

/** Do a PUT request to an API endpoint. */
put: IAPIRequest = (endpoint, data, auth, ignore, options = {}, apiVersion) => this.request('PUT', endpoint, data, auth, ignore, options, apiVersion)
put: IAPIRequest = (endpoint, data, ignore, options = {}, apiVersion) => this.request('PUT', endpoint, data, ignore, options, apiVersion)

/** Do a DELETE request to an API endpoint. */
del: IAPIRequest = (endpoint, data, auth, ignore, options = {}, apiVersion) => this.request('DELETE', endpoint, data, auth, ignore, options, apiVersion)
del: IAPIRequest = (endpoint, data, ignore, options = {}, apiVersion) => this.request('DELETE', endpoint, data, ignore, options, apiVersion)

/** Abort all current API requests, leaving the next request free to run. */
abort = (): void => {
Expand All @@ -287,7 +217,7 @@ export default class Api extends SDKEventEmitter {
}

async login (credentials: ILoginCredentials, args?: any): Promise<ILoginData | ILoginResult | null> {
const { data }: { data: ILoginData } = await this.post('login', { ...credentials, ...args }, false)
const { data }: { data: ILoginData } = await this.post('login', { ...credentials, ...args })
this.setLogin({
username: data.me.username ?? null,
userId: data.userId,
Expand Down Expand Up @@ -333,7 +263,7 @@ export default class Api extends SDKEventEmitter {
if (!this.currentLogin) {
return null
}
const result = await this.post('logout', {}, true)
const result = await this.post('logout', {})
this.clearLogin()
return result
}
Expand Down
21 changes: 16 additions & 5 deletions lib/clients/__tests__/Rocketchat.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,18 +175,29 @@ describe('client.logout', () => {
restClient.lastRequest().resolve({ status: 200, data: {} })
await pending

return client
return { client, restClient }
}

it('clears the REST auth headers', async () => {
expect((await loggedOutClient()).client.headers).not.toHaveProperty('X-Auth-Token')
const { client } = await loggedOutClient()

expect(client.client.headers).not.toHaveProperty('X-Auth-Token')
})

it('leaves the guard refusing an authenticated request', async () => {
const client = await loggedOutClient()
it('reports itself logged out', async () => {
const { client } = await loggedOutClient()

expect(client.loggedIn()).toBe(false)
await expect(client.get('me', {})).rejects.toThrow(/requires a login/)
})

it('still sends an Endpoint to the REST client', async () => {
const { client, restClient } = await loggedOutClient()

const pending = client.get('me', {})
restClient.lastRequest().resolve({ status: 200, data: { success: true } })
await pending

expect(restClient.lastRequest().endpoint).toBe('me')
})
})

Expand Down
Loading