@ -0,0 +1,30 @@
|
||||
import test, { expect } from '@playwright/test'
|
||||
import cuid from 'cuid'
|
||||
import { createTypebots } from 'utils/playwright/databaseActions'
|
||||
import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers'
|
||||
import { defaultChatwootOptions, IntegrationBlockType } from 'models'
|
||||
|
||||
const typebotId = cuid()
|
||||
|
||||
const chatwootTestWebsiteToken = 'tueXiiqEmrWUCZ4NUyoR7nhE'
|
||||
|
||||
test('should work as expected', async ({ page }) => {
|
||||
await createTypebots([
|
||||
{
|
||||
id: typebotId,
|
||||
...parseDefaultGroupWithBlock(
|
||||
{
|
||||
type: IntegrationBlockType.CHATWOOT,
|
||||
options: {
|
||||
...defaultChatwootOptions,
|
||||
websiteToken: chatwootTestWebsiteToken,
|
||||
},
|
||||
},
|
||||
{ withGoButton: true }
|
||||
),
|
||||
},
|
||||
])
|
||||
await page.goto(`/next/${typebotId}-public`)
|
||||
await page.getByRole('button', { name: 'Go' }).click()
|
||||
await expect(page.locator('#chatwoot_live_chat_widget')).toBeVisible()
|
||||
})
|
@ -1,9 +1,16 @@
|
||||
import { SessionState, GoogleSheetsGetOptions, VariableWithValue } from 'models'
|
||||
import { saveErrorLog, saveSuccessLog } from '@/features/logs/api'
|
||||
import {
|
||||
SessionState,
|
||||
GoogleSheetsGetOptions,
|
||||
VariableWithValue,
|
||||
ComparisonOperators,
|
||||
LogicalOperator,
|
||||
} from 'models'
|
||||
import { saveErrorLog } from '@/features/logs/api'
|
||||
import { getAuthenticatedGoogleDoc } from './helpers'
|
||||
import { parseVariables, updateVariables } from '@/features/variables'
|
||||
import { isNotEmpty, byId } from 'utils'
|
||||
import { updateVariables } from '@/features/variables'
|
||||
import { isNotEmpty, byId, isDefined } from 'utils'
|
||||
import { ExecuteIntegrationResponse } from '@/features/chat'
|
||||
import type { GoogleSpreadsheetRow } from 'google-spreadsheet'
|
||||
|
||||
export const getRow = async (
|
||||
state: SessionState,
|
||||
@ -12,56 +19,51 @@ export const getRow = async (
|
||||
options,
|
||||
}: { outgoingEdgeId?: string; options: GoogleSheetsGetOptions }
|
||||
): Promise<ExecuteIntegrationResponse> => {
|
||||
const { sheetId, cellsToExtract, referenceCell } = options
|
||||
const { sheetId, cellsToExtract, referenceCell, filter } = options
|
||||
if (!cellsToExtract || !sheetId || !referenceCell) return { outgoingEdgeId }
|
||||
|
||||
const variables = state.typebot.variables
|
||||
const resultId = state.result.id
|
||||
const resultId = state.result?.id
|
||||
|
||||
const doc = await getAuthenticatedGoogleDoc({
|
||||
credentialsId: options.credentialsId,
|
||||
spreadsheetId: options.spreadsheetId,
|
||||
})
|
||||
|
||||
const parsedReferenceCell = {
|
||||
column: referenceCell.column,
|
||||
value: parseVariables(variables)(referenceCell.value),
|
||||
}
|
||||
|
||||
const extractingColumns = cellsToExtract
|
||||
.map((cell) => cell.column)
|
||||
.filter(isNotEmpty)
|
||||
|
||||
try {
|
||||
await doc.loadInfo()
|
||||
const sheet = doc.sheetsById[sheetId]
|
||||
const rows = await sheet.getRows()
|
||||
const row = rows.find(
|
||||
(row) =>
|
||||
row[parsedReferenceCell.column as string] === parsedReferenceCell.value
|
||||
const filteredRows = rows.filter((row) =>
|
||||
referenceCell
|
||||
? row[referenceCell.column as string] === referenceCell.value
|
||||
: matchFilter(row, filter)
|
||||
)
|
||||
if (!row) {
|
||||
if (filteredRows.length === 0) {
|
||||
await saveErrorLog({
|
||||
resultId,
|
||||
message: "Couldn't find reference cell",
|
||||
})
|
||||
return { outgoingEdgeId }
|
||||
}
|
||||
const data: { [key: string]: string } = {
|
||||
...extractingColumns.reduce(
|
||||
(obj, column) => ({ ...obj, [column]: row[column] }),
|
||||
{}
|
||||
),
|
||||
}
|
||||
await saveSuccessLog({
|
||||
resultId,
|
||||
message: 'Succesfully fetched spreadsheet data',
|
||||
})
|
||||
const randomIndex = Math.floor(Math.random() * filteredRows.length)
|
||||
const extractingColumns = cellsToExtract
|
||||
.map((cell) => cell.column)
|
||||
.filter(isNotEmpty)
|
||||
const selectedRow = filteredRows
|
||||
.map((row) =>
|
||||
extractingColumns.reduce<{ [key: string]: string }>(
|
||||
(obj, column) => ({ ...obj, [column]: row[column] }),
|
||||
{}
|
||||
)
|
||||
)
|
||||
.at(randomIndex)
|
||||
if (!selectedRow) return { outgoingEdgeId }
|
||||
|
||||
const newVariables = options.cellsToExtract.reduce<VariableWithValue[]>(
|
||||
(newVariables, cell) => {
|
||||
const existingVariable = variables.find(byId(cell.variableId))
|
||||
const value = data[cell.column ?? ''] ?? null
|
||||
const value = selectedRow[cell.column ?? ''] ?? null
|
||||
if (!existingVariable) return newVariables
|
||||
return [
|
||||
...newVariables,
|
||||
@ -87,3 +89,56 @@ export const getRow = async (
|
||||
}
|
||||
return { outgoingEdgeId }
|
||||
}
|
||||
|
||||
const matchFilter = (
|
||||
row: GoogleSpreadsheetRow,
|
||||
filter: GoogleSheetsGetOptions['filter']
|
||||
) => {
|
||||
return filter.logicalOperator === LogicalOperator.AND
|
||||
? filter.comparisons.every(
|
||||
(comparison) =>
|
||||
comparison.column &&
|
||||
matchComparison(
|
||||
row[comparison.column],
|
||||
comparison.comparisonOperator,
|
||||
comparison.value
|
||||
)
|
||||
)
|
||||
: filter.comparisons.some(
|
||||
(comparison) =>
|
||||
comparison.column &&
|
||||
matchComparison(
|
||||
row[comparison.column],
|
||||
comparison.comparisonOperator,
|
||||
comparison.value
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const matchComparison = (
|
||||
inputValue?: string,
|
||||
comparisonOperator?: ComparisonOperators,
|
||||
value?: string
|
||||
) => {
|
||||
if (!inputValue || !comparisonOperator || !value) return false
|
||||
switch (comparisonOperator) {
|
||||
case ComparisonOperators.CONTAINS: {
|
||||
return inputValue.toLowerCase().includes(value.toLowerCase())
|
||||
}
|
||||
case ComparisonOperators.EQUAL: {
|
||||
return inputValue === value
|
||||
}
|
||||
case ComparisonOperators.NOT_EQUAL: {
|
||||
return inputValue !== value
|
||||
}
|
||||
case ComparisonOperators.GREATER: {
|
||||
return parseFloat(inputValue) > parseFloat(value)
|
||||
}
|
||||
case ComparisonOperators.LESS: {
|
||||
return parseFloat(inputValue) < parseFloat(value)
|
||||
}
|
||||
case ComparisonOperators.IS_SET: {
|
||||
return isDefined(inputValue) && inputValue.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,16 +23,18 @@ export const insertRow = async (
|
||||
await doc.loadInfo()
|
||||
const sheet = doc.sheetsById[options.sheetId]
|
||||
await sheet.addRow(parsedValues)
|
||||
await saveSuccessLog({
|
||||
resultId: result.id,
|
||||
message: 'Succesfully inserted row',
|
||||
})
|
||||
result &&
|
||||
(await saveSuccessLog({
|
||||
resultId: result.id,
|
||||
message: 'Succesfully inserted row',
|
||||
}))
|
||||
} catch (err) {
|
||||
await saveErrorLog({
|
||||
resultId: result.id,
|
||||
message: "Couldn't fetch spreadsheet data",
|
||||
details: err,
|
||||
})
|
||||
result &&
|
||||
(await saveErrorLog({
|
||||
resultId: result.id,
|
||||
message: "Couldn't fetch spreadsheet data",
|
||||
details: err,
|
||||
}))
|
||||
}
|
||||
return { outgoingEdgeId }
|
||||
}
|
||||
|
@ -45,16 +45,18 @@ export const updateRow = async (
|
||||
rows[updatingRowIndex][key] = parsedValues[key]
|
||||
}
|
||||
await rows[updatingRowIndex].save()
|
||||
await saveSuccessLog({
|
||||
resultId: result.id,
|
||||
message: 'Succesfully updated row',
|
||||
})
|
||||
result &&
|
||||
(await saveSuccessLog({
|
||||
resultId: result.id,
|
||||
message: 'Succesfully updated row',
|
||||
}))
|
||||
} catch (err) {
|
||||
await saveErrorLog({
|
||||
resultId: result.id,
|
||||
message: "Couldn't fetch spreadsheet data",
|
||||
details: err,
|
||||
})
|
||||
result &&
|
||||
(await saveErrorLog({
|
||||
resultId: result.id,
|
||||
message: "Couldn't fetch spreadsheet data",
|
||||
details: err,
|
||||
}))
|
||||
}
|
||||
return { outgoingEdgeId }
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ export const executeSendEmailBlock = async (
|
||||
const { variables } = typebot
|
||||
await sendEmail({
|
||||
typebotId: typebot.id,
|
||||
resultId: result.id,
|
||||
resultId: result?.id,
|
||||
credentialsId: options.credentialsId,
|
||||
recipients: options.recipients.map(parseVariables(variables)),
|
||||
subject: parseVariables(variables)(options.subject ?? ''),
|
||||
@ -59,7 +59,7 @@ const sendEmail = async ({
|
||||
fileUrls,
|
||||
}: SendEmailOptions & {
|
||||
typebotId: string
|
||||
resultId: string
|
||||
resultId?: string
|
||||
fileUrls?: string
|
||||
}) => {
|
||||
const { name: replyToName } = parseEmailRecipient(replyTo)
|
||||
@ -114,7 +114,7 @@ const sendEmail = async ({
|
||||
...emailBody,
|
||||
}
|
||||
try {
|
||||
const info = await transporter.sendMail(email)
|
||||
await transporter.sendMail(email)
|
||||
await saveSuccessLog({
|
||||
resultId,
|
||||
message: 'Email successfully sent',
|
||||
@ -169,7 +169,7 @@ const getEmailBody = async ({
|
||||
resultId,
|
||||
}: {
|
||||
typebotId: string
|
||||
resultId: string
|
||||
resultId?: string
|
||||
} & Pick<SendEmailOptions, 'isCustomBody' | 'isBodyCode' | 'body'>): Promise<
|
||||
{ html?: string; text?: string } | undefined
|
||||
> => {
|
||||
|
@ -0,0 +1,40 @@
|
||||
import test, { expect } from '@playwright/test'
|
||||
import { createSmtpCredentials } from '../../../../test/utils/databaseActions'
|
||||
import cuid from 'cuid'
|
||||
import { SmtpCredentialsData } from 'models'
|
||||
import { importTypebotInDatabase } from 'utils/playwright/databaseActions'
|
||||
import { getTestAsset } from '@/test/utils/playwright'
|
||||
|
||||
const mockSmtpCredentials: SmtpCredentialsData = {
|
||||
from: {
|
||||
email: 'marley.cummings@ethereal.email',
|
||||
name: 'Marley Cummings',
|
||||
},
|
||||
host: 'smtp.ethereal.email',
|
||||
port: 587,
|
||||
username: 'marley.cummings@ethereal.email',
|
||||
password: 'E5W1jHbAmv5cXXcut2',
|
||||
}
|
||||
|
||||
test.beforeAll(async () => {
|
||||
try {
|
||||
const credentialsId = 'send-email-credentials'
|
||||
await createSmtpCredentials(credentialsId, mockSmtpCredentials)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
})
|
||||
|
||||
test('should send an email', async ({ page }) => {
|
||||
const typebotId = cuid()
|
||||
await importTypebotInDatabase(getTestAsset('typebots/sendEmail.json'), {
|
||||
id: typebotId,
|
||||
publicId: `${typebotId}-public`,
|
||||
})
|
||||
await page.goto(`/next/${typebotId}-public`)
|
||||
await page.locator('text=Send email').click()
|
||||
await expect(page.getByText('Email sent!')).toBeVisible()
|
||||
await page.goto(`${process.env.NEXTAUTH_URL}/typebots/${typebotId}/results`)
|
||||
await page.click('text="See logs"')
|
||||
await expect(page.locator('text="Email successfully sent"')).toBeVisible()
|
||||
})
|
@ -35,37 +35,46 @@ export const executeWebhookBlock = async (
|
||||
where: { id: block.webhookId },
|
||||
})) as Webhook | null
|
||||
if (!webhook) {
|
||||
await saveErrorLog({
|
||||
resultId: result.id,
|
||||
message: `Couldn't find webhook`,
|
||||
})
|
||||
result &&
|
||||
(await saveErrorLog({
|
||||
resultId: result.id,
|
||||
message: `Couldn't find webhook`,
|
||||
}))
|
||||
return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
}
|
||||
const preparedWebhook = prepareWebhookAttributes(webhook, block.options)
|
||||
const resultValues = await getResultValues(result.id)
|
||||
const resultValues = result && (await getResultValues(result.id))
|
||||
if (!resultValues) return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
const webhookResponse = await executeWebhook({ typebot })(
|
||||
preparedWebhook,
|
||||
typebot.variables,
|
||||
block.groupId,
|
||||
resultValues,
|
||||
result.id
|
||||
result?.id
|
||||
)
|
||||
const status = webhookResponse.statusCode.toString()
|
||||
const isError = status.startsWith('4') || status.startsWith('5')
|
||||
|
||||
if (isError) {
|
||||
await saveErrorLog({
|
||||
resultId: result.id,
|
||||
message: `Webhook returned error: ${webhookResponse.data}`,
|
||||
details: JSON.stringify(webhookResponse.data, null, 2).substring(0, 1000),
|
||||
})
|
||||
result &&
|
||||
(await saveErrorLog({
|
||||
resultId: result.id,
|
||||
message: `Webhook returned error: ${webhookResponse.data}`,
|
||||
details: JSON.stringify(webhookResponse.data, null, 2).substring(
|
||||
0,
|
||||
1000
|
||||
),
|
||||
}))
|
||||
} else {
|
||||
await saveSuccessLog({
|
||||
resultId: result.id,
|
||||
message: `Webhook returned success: ${webhookResponse.data}`,
|
||||
details: JSON.stringify(webhookResponse.data, null, 2).substring(0, 1000),
|
||||
})
|
||||
result &&
|
||||
(await saveSuccessLog({
|
||||
resultId: result.id,
|
||||
message: `Webhook returned success: ${webhookResponse.data}`,
|
||||
details: JSON.stringify(webhookResponse.data, null, 2).substring(
|
||||
0,
|
||||
1000
|
||||
),
|
||||
}))
|
||||
}
|
||||
|
||||
const newVariables = block.options.responseVariableMapping.reduce<
|
||||
@ -265,6 +274,7 @@ const convertKeyValueTableToObject = (
|
||||
}, {})
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const safeJsonParse = (json: string): { data: any; isJson: boolean } => {
|
||||
try {
|
||||
return { data: JSON.parse(json), isJson: true }
|
||||
|
@ -20,29 +20,33 @@ test.describe('Bot', () => {
|
||||
publicId: `${typebotId}-public`,
|
||||
})
|
||||
|
||||
await createWebhook(typebotId, {
|
||||
id: 'failing-webhook',
|
||||
url: 'http://localhost:3001/api/mock/fail',
|
||||
method: HttpMethod.POST,
|
||||
})
|
||||
try {
|
||||
await createWebhook(typebotId, {
|
||||
id: 'failing-webhook',
|
||||
url: 'http://localhost:3001/api/mock/fail',
|
||||
method: HttpMethod.POST,
|
||||
})
|
||||
|
||||
await createWebhook(typebotId, {
|
||||
id: 'partial-body-webhook',
|
||||
url: 'http://localhost:3000/api/mock/webhook-easy-config',
|
||||
method: HttpMethod.POST,
|
||||
body: `{
|
||||
await createWebhook(typebotId, {
|
||||
id: 'partial-body-webhook',
|
||||
url: 'http://localhost:3000/api/mock/webhook-easy-config',
|
||||
method: HttpMethod.POST,
|
||||
body: `{
|
||||
"name": "{{Name}}",
|
||||
"age": {{Age}},
|
||||
"gender": "{{Gender}}"
|
||||
}`,
|
||||
})
|
||||
})
|
||||
|
||||
await createWebhook(typebotId, {
|
||||
id: 'full-body-webhook',
|
||||
url: 'http://localhost:3000/api/mock/webhook-easy-config',
|
||||
method: HttpMethod.POST,
|
||||
body: `{{Full body}}`,
|
||||
})
|
||||
await createWebhook(typebotId, {
|
||||
id: 'full-body-webhook',
|
||||
url: 'http://localhost:3000/api/mock/webhook-easy-config',
|
||||
method: HttpMethod.POST,
|
||||
body: `{{Full body}}`,
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
|
||||
test.afterEach(async () => {
|
||||
|
@ -0,0 +1,78 @@
|
||||
import test, { expect } from '@playwright/test'
|
||||
import cuid from 'cuid'
|
||||
import { HttpMethod } from 'models'
|
||||
import {
|
||||
createWebhook,
|
||||
deleteTypebots,
|
||||
deleteWebhooks,
|
||||
importTypebotInDatabase,
|
||||
} from 'utils/playwright/databaseActions'
|
||||
import { getTestAsset } from '@/test/utils/playwright'
|
||||
|
||||
const typebotId = cuid()
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await importTypebotInDatabase(getTestAsset('typebots/webhook.json'), {
|
||||
id: typebotId,
|
||||
publicId: `${typebotId}-public`,
|
||||
})
|
||||
|
||||
try {
|
||||
await createWebhook(typebotId, {
|
||||
id: 'failing-webhook',
|
||||
url: 'http://localhost:3001/api/mock/fail',
|
||||
method: HttpMethod.POST,
|
||||
})
|
||||
|
||||
await createWebhook(typebotId, {
|
||||
id: 'partial-body-webhook',
|
||||
url: 'http://localhost:3000/api/mock/webhook-easy-config',
|
||||
method: HttpMethod.POST,
|
||||
body: `{
|
||||
"name": "{{Name}}",
|
||||
"age": {{Age}},
|
||||
"gender": "{{Gender}}"
|
||||
}`,
|
||||
})
|
||||
|
||||
await createWebhook(typebotId, {
|
||||
id: 'full-body-webhook',
|
||||
url: 'http://localhost:3000/api/mock/webhook-easy-config',
|
||||
method: HttpMethod.POST,
|
||||
body: `{{Full body}}`,
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
|
||||
test.afterEach(async () => {
|
||||
await deleteTypebots([typebotId])
|
||||
await deleteWebhooks([
|
||||
'failing-webhook',
|
||||
'partial-body-webhook',
|
||||
'full-body-webhook',
|
||||
])
|
||||
})
|
||||
|
||||
test('should execute webhooks properly', async ({ page }) => {
|
||||
await page.goto(`/next/${typebotId}-public`)
|
||||
await page.locator('text=Send failing webhook').click()
|
||||
await page.locator('[placeholder="Type a name..."]').fill('John')
|
||||
await page.locator('text="Send"').click()
|
||||
await page.locator('[placeholder="Type an age..."]').fill('30')
|
||||
await page.locator('text="Send"').click()
|
||||
await page.locator('text="Male"').click()
|
||||
await expect(
|
||||
page.getByText('{"name":"John","age":25,"gender":"male"}')
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
page.getByText('{"name":"John","age":30,"gender":"Male"}')
|
||||
).toBeVisible()
|
||||
await page.goto(`http://localhost:3000/typebots/${typebotId}/results`)
|
||||
await page.click('text="See logs"')
|
||||
await expect(
|
||||
page.locator('text="Webhook successfuly executed." >> nth=1')
|
||||
).toBeVisible()
|
||||
await expect(page.locator('text="Webhook returned an error"')).toBeVisible()
|
||||
})
|
Reference in New Issue
Block a user