@@ -1,4 +1,4 @@
|
||||
const emailRegex =
|
||||
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
|
||||
export const validateEmail = (email: string) => emailRegex.test(email)
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import test, { expect } from '@playwright/test'
|
||||
import cuid from 'cuid'
|
||||
import { parse } from 'papaparse'
|
||||
import { readFileSync } from 'fs'
|
||||
import { isDefined } from 'utils'
|
||||
import {
|
||||
createWorkspaces,
|
||||
importTypebotInDatabase,
|
||||
injectFakeResults,
|
||||
} from 'utils/playwright/databaseActions'
|
||||
import { getTestAsset } from '@/test/utils/playwright'
|
||||
import { Plan } from 'db'
|
||||
|
||||
const THREE_GIGABYTES = 3 * 1024 * 1024 * 1024
|
||||
|
||||
test('should work as expected', async ({ page, browser }) => {
|
||||
const typebotId = cuid()
|
||||
await importTypebotInDatabase(getTestAsset('typebots/fileUpload.json'), {
|
||||
id: typebotId,
|
||||
publicId: `${typebotId}-public`,
|
||||
})
|
||||
await page.goto(`/next/${typebotId}-public`)
|
||||
await page
|
||||
.locator(`input[type="file"]`)
|
||||
.setInputFiles([
|
||||
getTestAsset('typebots/api.json'),
|
||||
getTestAsset('typebots/fileUpload.json'),
|
||||
getTestAsset('typebots/hugeGroup.json'),
|
||||
])
|
||||
await expect(page.locator(`text="3"`)).toBeVisible()
|
||||
await page.locator('text="Upload 3 files"').click()
|
||||
await expect(page.locator(`text="3 files uploaded"`)).toBeVisible()
|
||||
await page.goto(`${process.env.NEXTAUTH_URL}/typebots/${typebotId}/results`)
|
||||
await expect(page.getByRole('link', { name: 'api.json' })).toHaveAttribute(
|
||||
'href',
|
||||
/.+\/api\.json/
|
||||
)
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'fileUpload.json' })
|
||||
).toHaveAttribute('href', /.+\/fileUpload\.json/)
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'hugeGroup.json' })
|
||||
).toHaveAttribute('href', /.+\/hugeGroup\.json/)
|
||||
|
||||
await page.click('[data-testid="checkbox"] >> nth=0')
|
||||
const [download] = await Promise.all([
|
||||
page.waitForEvent('download'),
|
||||
page.locator('text="Export"').click(),
|
||||
])
|
||||
const downloadPath = await download.path()
|
||||
expect(downloadPath).toBeDefined()
|
||||
const file = readFileSync(downloadPath as string).toString()
|
||||
const { data } = parse(file)
|
||||
expect(data).toHaveLength(2)
|
||||
expect((data[1] as unknown[])[1]).toContain(process.env.S3_ENDPOINT)
|
||||
|
||||
const urls = (
|
||||
await Promise.all(
|
||||
[
|
||||
page.getByRole('link', { name: 'api.json' }),
|
||||
page.getByRole('link', { name: 'fileUpload.json' }),
|
||||
page.getByRole('link', { name: 'hugeGroup.json' }),
|
||||
].map((elem) => elem.getAttribute('href'))
|
||||
)
|
||||
).filter(isDefined)
|
||||
|
||||
const page2 = await browser.newPage()
|
||||
await page2.goto(urls[0])
|
||||
await expect(page2.locator('pre')).toBeVisible()
|
||||
|
||||
await page.locator('button >> text="Delete"').click()
|
||||
await page.locator('button >> text="Delete" >> nth=1').click()
|
||||
await expect(page.locator('text="api.json"')).toBeHidden()
|
||||
await page2.goto(urls[0])
|
||||
await expect(page2.locator('pre')).toBeHidden()
|
||||
})
|
||||
|
||||
test.describe('Storage limit is reached', () => {
|
||||
const typebotId = cuid()
|
||||
const workspaceId = cuid()
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await createWorkspaces([{ id: workspaceId, plan: Plan.STARTER }])
|
||||
await importTypebotInDatabase(getTestAsset('typebots/fileUpload.json'), {
|
||||
id: typebotId,
|
||||
publicId: `${typebotId}-public`,
|
||||
workspaceId,
|
||||
})
|
||||
await injectFakeResults({
|
||||
typebotId,
|
||||
count: 20,
|
||||
fakeStorage: THREE_GIGABYTES,
|
||||
})
|
||||
})
|
||||
|
||||
test("shouldn't upload anything if limit has been reached", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto(`/next/${typebotId}-public`)
|
||||
await page
|
||||
.locator(`input[type="file"]`)
|
||||
.setInputFiles([
|
||||
getTestAsset('typebots/api.json'),
|
||||
getTestAsset('typebots/fileUpload.json'),
|
||||
getTestAsset('typebots/hugeGroup.json'),
|
||||
])
|
||||
await expect(page.locator(`text="3"`)).toBeVisible()
|
||||
await page.locator('text="Upload 3 files"').click()
|
||||
await expect(page.locator(`text="3 files uploaded"`)).toBeVisible()
|
||||
await page.evaluate(() =>
|
||||
window.localStorage.setItem('workspaceId', 'starterWorkspace')
|
||||
)
|
||||
await page.goto(`${process.env.NEXTAUTH_URL}/typebots/${typebotId}/results`)
|
||||
await expect(page.locator('text="150%"')).toBeVisible()
|
||||
await expect(page.locator('text="api.json"')).toBeHidden()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
export * from './utils'
|
||||
@@ -0,0 +1,128 @@
|
||||
import { parseVariables } from '@/features/variables'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import {
|
||||
PaymentInputOptions,
|
||||
PaymentInputRuntimeOptions,
|
||||
SessionState,
|
||||
StripeCredentialsData,
|
||||
} from 'models'
|
||||
import Stripe from 'stripe'
|
||||
import { decrypt } from 'utils/api/encryption'
|
||||
|
||||
export const computePaymentInputRuntimeOptions =
|
||||
(state: SessionState) => (options: PaymentInputOptions) =>
|
||||
createStripePaymentIntent(state)(options)
|
||||
|
||||
const createStripePaymentIntent =
|
||||
(state: SessionState) =>
|
||||
async (options: PaymentInputOptions): Promise<PaymentInputRuntimeOptions> => {
|
||||
const {
|
||||
isPreview,
|
||||
typebot: { variables },
|
||||
} = state
|
||||
if (!options.credentialsId)
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Missing credentialsId',
|
||||
})
|
||||
const stripeKeys = await getStripeInfo(options.credentialsId)
|
||||
if (!stripeKeys)
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'Credentials not found',
|
||||
})
|
||||
const stripe = new Stripe(
|
||||
isPreview && stripeKeys?.test?.secretKey
|
||||
? stripeKeys.test.secretKey
|
||||
: stripeKeys.live.secretKey,
|
||||
{ apiVersion: '2022-11-15' }
|
||||
)
|
||||
const amount =
|
||||
Number(parseVariables(variables)(options.amount)) *
|
||||
(isZeroDecimalCurrency(options.currency) ? 1 : 100)
|
||||
if (isNaN(amount))
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message:
|
||||
'Could not parse amount, make sure your block is configured correctly',
|
||||
})
|
||||
// Create a PaymentIntent with the order amount and currency
|
||||
const receiptEmail = parseVariables(variables)(
|
||||
options.additionalInformation?.email
|
||||
)
|
||||
const paymentIntent = await stripe.paymentIntents.create({
|
||||
amount,
|
||||
currency: options.currency,
|
||||
receipt_email: receiptEmail === '' ? undefined : receiptEmail,
|
||||
automatic_payment_methods: {
|
||||
enabled: true,
|
||||
},
|
||||
})
|
||||
|
||||
if (!paymentIntent.client_secret)
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Could not create payment intent',
|
||||
})
|
||||
|
||||
return {
|
||||
paymentIntentSecret: paymentIntent.client_secret,
|
||||
publicKey:
|
||||
isPreview && stripeKeys.test?.publicKey
|
||||
? stripeKeys.test.publicKey
|
||||
: stripeKeys.live.publicKey,
|
||||
amountLabel: `${
|
||||
amount / (isZeroDecimalCurrency(options.currency) ? 1 : 100)
|
||||
}${currencySymbols[options.currency] ?? ` ${options.currency}`}`,
|
||||
}
|
||||
}
|
||||
|
||||
const getStripeInfo = async (
|
||||
credentialsId: string
|
||||
): Promise<StripeCredentialsData | undefined> => {
|
||||
const credentials = await prisma.credentials.findUnique({
|
||||
where: { id: credentialsId },
|
||||
})
|
||||
if (!credentials) return
|
||||
return decrypt(credentials.data, credentials.iv) as StripeCredentialsData
|
||||
}
|
||||
|
||||
// https://stripe.com/docs/currencies#zero-decimal
|
||||
const isZeroDecimalCurrency = (currency: string) =>
|
||||
[
|
||||
'BIF',
|
||||
'CLP',
|
||||
'DJF',
|
||||
'GNF',
|
||||
'JPY',
|
||||
'KMF',
|
||||
'KRW',
|
||||
'MGA',
|
||||
'PYG',
|
||||
'RWF',
|
||||
'UGX',
|
||||
'VND',
|
||||
'VUV',
|
||||
'XAF',
|
||||
'XOF',
|
||||
'XPF',
|
||||
].includes(currency)
|
||||
|
||||
const currencySymbols: { [key: string]: string } = {
|
||||
USD: '$',
|
||||
EUR: '€',
|
||||
CRC: '₡',
|
||||
GBP: '£',
|
||||
ILS: '₪',
|
||||
INR: '₹',
|
||||
JPY: '¥',
|
||||
KRW: '₩',
|
||||
NGN: '₦',
|
||||
PHP: '₱',
|
||||
PLN: 'zł',
|
||||
PYG: '₲',
|
||||
THB: '฿',
|
||||
UAH: '₴',
|
||||
VND: '₫',
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './computePaymentInputRuntimeOptions'
|
||||
@@ -1 +1 @@
|
||||
export { validatePhoneNumber } from './utils/validatePhoneNumber'
|
||||
export * from './utils'
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import phone from 'phone'
|
||||
|
||||
export const formatPhoneNumber = (phoneNumber: string) =>
|
||||
phone(phoneNumber).phoneNumber
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './formatPhoneNumber'
|
||||
export * from './validatePhoneNumber'
|
||||
@@ -1,4 +1,4 @@
|
||||
const phoneRegex = /^\+?[0-9]{6,}$/
|
||||
import { phone } from 'phone'
|
||||
|
||||
export const validatePhoneNumber = (phoneNumber: string) =>
|
||||
phoneRegex.test(phoneNumber)
|
||||
phone(phoneNumber).isValid
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
@@ -10,7 +10,9 @@ export const executeRedirect = (
|
||||
if (!block.options?.url) return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
const formattedUrl = sanitizeUrl(parseVariables(variables)(block.options.url))
|
||||
return {
|
||||
logic: { redirectUrl: formattedUrl },
|
||||
logic: {
|
||||
redirect: { url: formattedUrl, isNewTab: block.options.isNewTab },
|
||||
},
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { ExecuteLogicResponse } from '@/features/chat'
|
||||
import { saveErrorLog } from '@/features/logs/api'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { TypebotLinkBlock, Edge, SessionState, TypebotInSession } from 'models'
|
||||
import {
|
||||
TypebotLinkBlock,
|
||||
Edge,
|
||||
SessionState,
|
||||
TypebotInSession,
|
||||
Variable,
|
||||
} from 'models'
|
||||
import { byId } from 'utils'
|
||||
|
||||
export const executeTypebotLink = async (
|
||||
@@ -9,20 +15,22 @@ export const executeTypebotLink = async (
|
||||
block: TypebotLinkBlock
|
||||
): Promise<ExecuteLogicResponse> => {
|
||||
if (!block.options.typebotId) {
|
||||
saveErrorLog({
|
||||
resultId: state.result.id,
|
||||
message: 'Failed to link typebot',
|
||||
details: 'Typebot ID is not specified',
|
||||
})
|
||||
state.result &&
|
||||
saveErrorLog({
|
||||
resultId: state.result.id,
|
||||
message: 'Failed to link typebot',
|
||||
details: 'Typebot ID is not specified',
|
||||
})
|
||||
return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
}
|
||||
const linkedTypebot = await getLinkedTypebot(state, block.options.typebotId)
|
||||
if (!linkedTypebot) {
|
||||
saveErrorLog({
|
||||
resultId: state.result.id,
|
||||
message: 'Failed to link typebot',
|
||||
details: `Typebot with ID ${block.options.typebotId} not found`,
|
||||
})
|
||||
state.result &&
|
||||
saveErrorLog({
|
||||
resultId: state.result.id,
|
||||
message: 'Failed to link typebot',
|
||||
details: `Typebot with ID ${block.options.typebotId} not found`,
|
||||
})
|
||||
return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
}
|
||||
let newSessionState = addLinkedTypebotToState(state, block, linkedTypebot)
|
||||
@@ -32,11 +40,12 @@ export const executeTypebotLink = async (
|
||||
linkedTypebot.groups.find((b) => b.blocks.some((s) => s.type === 'start'))
|
||||
?.id
|
||||
if (!nextGroupId) {
|
||||
saveErrorLog({
|
||||
resultId: state.result.id,
|
||||
message: 'Failed to link typebot',
|
||||
details: `Group with ID "${block.options.groupId}" not found`,
|
||||
})
|
||||
state.result &&
|
||||
saveErrorLog({
|
||||
resultId: state.result.id,
|
||||
message: 'Failed to link typebot',
|
||||
details: `Group with ID "${block.options.groupId}" not found`,
|
||||
})
|
||||
return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
}
|
||||
const portalEdge: Edge = {
|
||||
@@ -65,29 +74,50 @@ const addLinkedTypebotToState = (
|
||||
state: SessionState,
|
||||
block: TypebotLinkBlock,
|
||||
linkedTypebot: TypebotInSession
|
||||
): SessionState => ({
|
||||
...state,
|
||||
typebot: {
|
||||
...state.typebot,
|
||||
groups: [...state.typebot.groups, ...linkedTypebot.groups],
|
||||
variables: [...state.typebot.variables, ...linkedTypebot.variables],
|
||||
edges: [...state.typebot.edges, ...linkedTypebot.edges],
|
||||
},
|
||||
linkedTypebots: {
|
||||
typebots: [
|
||||
...state.linkedTypebots.typebots.filter(
|
||||
(existingTypebots) => existingTypebots.id !== linkedTypebot.id
|
||||
),
|
||||
],
|
||||
queue: block.outgoingEdgeId
|
||||
? [
|
||||
...state.linkedTypebots.queue,
|
||||
{ edgeId: block.outgoingEdgeId, typebotId: state.currentTypebotId },
|
||||
]
|
||||
: state.linkedTypebots.queue,
|
||||
},
|
||||
currentTypebotId: linkedTypebot.id,
|
||||
})
|
||||
): SessionState => {
|
||||
const incomingVariables = fillVariablesWithExistingValues(
|
||||
linkedTypebot.variables,
|
||||
state.typebot.variables
|
||||
)
|
||||
return {
|
||||
...state,
|
||||
typebot: {
|
||||
...state.typebot,
|
||||
groups: [...state.typebot.groups, ...linkedTypebot.groups],
|
||||
variables: [...state.typebot.variables, ...incomingVariables],
|
||||
edges: [...state.typebot.edges, ...linkedTypebot.edges],
|
||||
},
|
||||
linkedTypebots: {
|
||||
typebots: [
|
||||
...state.linkedTypebots.typebots.filter(
|
||||
(existingTypebots) => existingTypebots.id !== linkedTypebot.id
|
||||
),
|
||||
],
|
||||
queue: block.outgoingEdgeId
|
||||
? [
|
||||
...state.linkedTypebots.queue,
|
||||
{ edgeId: block.outgoingEdgeId, typebotId: state.currentTypebotId },
|
||||
]
|
||||
: state.linkedTypebots.queue,
|
||||
},
|
||||
currentTypebotId: linkedTypebot.id,
|
||||
}
|
||||
}
|
||||
|
||||
const fillVariablesWithExistingValues = (
|
||||
variables: Variable[],
|
||||
variablesWithValues: Variable[]
|
||||
): Variable[] =>
|
||||
variables.map((variable) => {
|
||||
const matchedVariable = variablesWithValues.find(
|
||||
(variableWithValue) => variableWithValue.name === variable.name
|
||||
)
|
||||
|
||||
return {
|
||||
...variable,
|
||||
value: matchedVariable?.value ?? variable.value,
|
||||
}
|
||||
})
|
||||
|
||||
const getLinkedTypebot = async (
|
||||
state: SessionState,
|
||||
|
||||
@@ -3,7 +3,7 @@ import test, { expect } from '@playwright/test'
|
||||
import { importTypebotInDatabase } from 'utils/playwright/databaseActions'
|
||||
import { typebotViewer } from 'utils/playwright/testHelpers'
|
||||
|
||||
const typebotId = 'cl0ibhi7s0018n21aarlmg0cm'
|
||||
const typebotId = 'cl0ibhi7s0018n21aarlmg0cm1'
|
||||
const linkedTypebotId = 'cl0ibhv8d0130n21aw8doxhj5'
|
||||
|
||||
test.beforeAll(async () => {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { getTestAsset } from '@/test/utils/playwright'
|
||||
import test, { expect } from '@playwright/test'
|
||||
import { importTypebotInDatabase } from 'utils/playwright/databaseActions'
|
||||
|
||||
const typebotId = 'cl0ibhi7s0018n21aarlmg0cm'
|
||||
const linkedTypebotId = 'cl0ibhv8d0130n21aw8doxhj5'
|
||||
|
||||
test.beforeAll(async () => {
|
||||
try {
|
||||
await importTypebotInDatabase(
|
||||
getTestAsset('typebots/linkTypebots/1.json'),
|
||||
{ id: typebotId, publicId: `${typebotId}-public` }
|
||||
)
|
||||
await importTypebotInDatabase(
|
||||
getTestAsset('typebots/linkTypebots/2.json'),
|
||||
{ id: linkedTypebotId, publicId: `${linkedTypebotId}-public` }
|
||||
)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
})
|
||||
|
||||
test('should work as expected', async ({ page }) => {
|
||||
await page.goto(`/next/${typebotId}-public`)
|
||||
await page.locator('input').fill('Hello there!')
|
||||
await page.locator('input').press('Enter')
|
||||
await expect(page.getByText('Cheers!')).toBeVisible()
|
||||
await page.goto(`${process.env.NEXTAUTH_URL}/typebots/${typebotId}/results`)
|
||||
await expect(page.locator('text=Hello there!')).toBeVisible()
|
||||
})
|
||||
Reference in New Issue
Block a user