Add Activepieces integration for workflow automation

- Add Activepieces fork with SmoothSchedule custom piece
- Create integrations app with Activepieces service layer
- Add embed token endpoint for iframe integration
- Create Automations page with embedded workflow builder
- Add sidebar visibility fix for embed mode
- Add list inactive customers endpoint to Public API
- Include SmoothSchedule triggers: event created/updated/cancelled
- Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -0,0 +1,139 @@
import { SigningKeyId } from '@activepieces/ee-shared'
import { apId, DefaultProjectRole, Principal } from '@activepieces/shared'
import { faker } from '@faker-js/faker'
import jwt, { Algorithm, JwtPayload, SignOptions } from 'jsonwebtoken'
import {
ExternalPrincipal,
ExternalTokenPayload,
} from '../../src/app/ee/managed-authn/lib/external-token-extractor'
const generateToken = ({
payload,
algorithm = 'HS256',
key = 'secret',
keyId = '1',
issuer = 'activepieces',
}: GenerateTokenParams): string => {
const options: SignOptions = {
algorithm,
expiresIn: '1h',
keyid: keyId,
issuer,
}
return jwt.sign(payload, key, options)
}
export const generateMockToken = async (
principal: Principal,
): Promise<string> => {
const mockPrincipal: Principal = principal
return generateToken({
payload: mockPrincipal,
issuer: 'activepieces',
})
}
const MOCK_SIGNING_KEY_PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY-----
MIIJKAIBAAKCAgEAlnd5vGP/1bzcndN/yRD+ZTd6tuemxaJd+12bOZ2QCXcTM03A
KSp3NE5QMyIi13PXMg+z1uPowfivPJ4iVTMaW1U00O7JlUduGR0VrG0BCJlfEf85
2V71TfE+2+EpMme9Yw6Gs/YAuOwgVwu3n/XF0il3FTIm1oY1a/MA79rv0RSscnIg
CaYJe86LWm+H6753Si0MIId/ajIfYYIndN6qRIlPsgagdL+kljUSPEiIzmV0POxT
ltBotXL1t7Mu+meJrY85MXG5W8BS05+q6dJql7Cl0UbPK152ziakB+biMI/4hYla
OIBT3KeOcz/Jg7Zv21Y0tbdrZ5osVrrNpFsCV7PGyQIUDVmmnCHrOEBS2XM5zOHz
TxMlJQh3Db318rB5415zuBTzrO+20++03kH4SwZEEBg1SDAInYwLOWldbTuZuD0H
x7P2g4a3OqHHVOcAgtsHgmU7/zCgCIETg4KbRdpSsqOm/YJDWWoLDTwvKnH5QHSB
acq1kxbNAUSuLQESkfZq1Dw5+tdBDJr29bxjmiSggyittTYn1B3iHACNoe4zj9sM
QQIfj9mmntXsa/leIwBVspiEOHYZwJOe5+goSd8K1VIQJxC1DVBxB2eHxMvuo3ey
J0HEDlebIeZy4zrE1LPgRic1kfdemyxvuN3iwZnPGiY79nL1ZNDM3M4ApSMCAwEA
AQKCAgA14VqqZ3S5aQPnUFE2AuvV+uPqk1FY/CeDV6W6H/3wJb+uY20oUJiXFmQJ
q3Omi0jIGG9hyAMVUqQNpOLOd5o8kmpzVs7Ase9u9sdIE1CHb8RngWmJuUNGQdks
i5hhAF0FF7KMxs7DaWq7QOrkUPIhq8+Eu4zEzRJcMYxoV5IA4NJPuSZXzikfOHsW
S1H0zSOSYEczbtHliUVLeXv/kayPFkx/h3f11pptX1vEUoUKw7G4DzhvjPmx4BS1
T2jHKkRW7i6g0gR6IoiGV2qwiDS7VPpL0ntlIFKSx6t9WOQuV5+60dCI4wskvKt6
AaF7lNzBQkFlwOSpGMA/3my9KgnQKQ7OyzBKuNlMuWkiw4aTLEjgoS6kkfhRhMSG
ks6Igzj+KdoLvjzO+seBCd8eLUYbQGrFzODwxKunrdYMPiaCf+MmAuRAwNtPQDd1
uyG+2/BmG8mr2qEVwV65DxLbAI7SRLdQ5RICAJ2CujujcPhA2Neu9Do5Vy4wbucH
KFpLgxo5SAkdCQ8IeErWNdM5IVUBITcWXBb/1tJLJyeC4D34M27ZPVSdokxwO/Yz
4DR+m7CKBktA9ZSnJ4OKN+LFM/lWkGjEpmuGn+Tsi2FSN7jPwTo86xiq2sHxxTzo
+Rmr15QVCPfkYky/3HI7lu8VaqJbtiU/XP6ue6vOGEFMu0McIQKCAQEAtgyp5tDT
t2vFmG1U7e9PGv3TiDVoFClVljtCrNV2EHNx//wm7VzZns4W9HZIvPc20aMgpYQ+
pk55VO8Tiys4wz9XXORhIUkWbHbWMfw2lrNuwHNG2UQ7uj51FvhJt3XVAFsHgr50
EmraDYz7NsrJ3TR3lnDlbzhGOBNvOtoIoDbF+Jle3Yi1NXk2WJF2So7xEfkEzHUi
O3caLCqfJd9zMOLTP3IF52JckJ0GppVVjAhgLll13MnRRrEJEP7w95/zPkIhJORk
6AJGjDbZJS0oBW8At/V2Q+ofaSGtMoAapjGjrANRtTU9zt4uiNqOeoyYNt0mq9xu
YnCmvgqrhkH0hwKCAQEA05aEk77oBqGUI+93Dxc9KzQcJd56TB5fu0xLRUHfySfO
whS4B4MiH5NkbuqLb5Ou3DROcVpGbu5pOlHtAsZg6CswgcbxwMEPbP7VPuKz30wj
z1tLVsSTqBMu0ZtbF7QuGL1EzYk0+godzHmOzq20vSr6N+TSiLMEjogcXVP9qOw+
HPJYxBjMbKRhAgVXE8bCDQXsBWsaWveQ9MkjJUj9otcI1ZJYLSxwaXl5OORX62cQ
odfaz01aned/fZifSvU4i5XQraCOEQKT+S2vGULsxxZ0ZS3xk3aSvffLpLD5LMVR
9nyYs/aeUqEP533RUAJb6LdVlg7yBouHtG3AC1BNhQKCAQA3WRBKuZC0wlJX7l2U
3V4KkcM/NSWIg6yeuTOjQl7bz42IS0w2fDU5n+TAvDmPIgYLpHHngJZfj5o55Vnm
xOREEDzqZBDXwtXLcjHbDpg2JyVz41hV8/XIwPZuXlxjJ7Lzobld2bOGafATkJpL
5UmMNEhrd7V5o/1NTTNTDDj1JNH5q/94kPiu4kRQlyEEuAK4+SGpW69lrudJKEgs
howJ/9xD/NGosHH+EY+VE+/nXCCJ1u8LilxTBr3/6dKvJnUYp5hWFA5Nr2ttc7t/
HwR86muogjtLmKGmH/P9V49CmfLt+DBeTGqXO1ughfotbhNVEtWQCLuSuDcprirJ
7cF/AoIBAQCkejArLc7uQLKI0MCrcXQyXnq3EV/eRgpC7cbhWpjcpN47zqFT7aMc
CpabBiZIIPRf5yVHRlbUKu6P0Fm+u3lfYRt+9qi9HxafsuUP0mji3yxDJ4PEOmFR
2T+e3vaL0Zu3zYFriQouiKirZ58UmMGT/5Gs22qxqv+S0MnD3uOjaanLFLTeEyzu
E0X5rS8Ih4wXVZAokh5VsnbzYlu4wymvaRtL8kwrKY1k4HHUQOT7cA3k0Ygdd9NG
Rku71WWWflNrZpVmMxXcsTVYESQ5LeYjyRfIA1P0PstJcxPRvWSlYeoaArctxjtC
nkNfv1VzrbHGkKWuVYXcgqCGKH6ODOmFAoIBAC1CcPpZ3HbsCWdai/sMqF6mp9IH
SDftZGia3OqO5Qsf1je1RRPTAANQe5aPowP1Q53uytFS+jGMJztqNxRzPqiW/88D
zEsIUkCfgpHHIVyE500la8Mo+HtqnfcqAgCvBn2tH2LrHePyVFhOdbCmfT4V649H
U9WK9LNxhh/g0/wItgNdbM6pbBANU71jAMhb1cLCH1/muclC6ZyYpC+1pBZm50FO
GsXpDpzyyhR2ZjY3b+bLfTT9YOyGIxBp4hqA3Rcc6c7l31lAAsDxtfWfXeoMLt5T
CEri0OurQ6fh4y87TK4JFbSTPEDkrPh4STPH7TtroBM/rn7Zj4+1Ur1RlgI=
-----END RSA PRIVATE KEY-----`
export const generateMockExternalToken = (
params?: Partial<GenerateMockExternalTokenParams>,
): GenerateMockExternalTokenReturn => {
const mockExternalTokenPayload: ExternalTokenPayload = {
externalUserId: params?.externalUserId ?? apId(),
role: params?.projectRole as DefaultProjectRole ?? DefaultProjectRole.ADMIN,
externalProjectId: params?.externalProjectId ?? apId(),
firstName: params?.externalFirstName ?? faker.person.firstName(),
pieces: params?.pieces ?? undefined,
lastName: params?.externalLastName ?? faker.person.lastName(),
}
const algorithm = 'RS256'
const key = params?.privateKey ?? MOCK_SIGNING_KEY_PRIVATE_KEY
const keyId = params?.signingKeyId ?? apId()
const mockExternalToken = generateToken({
payload: mockExternalTokenPayload,
algorithm,
key,
keyId,
})
return {
mockExternalToken,
mockExternalTokenPayload,
}
}
export const decodeToken = (token: string): JwtPayload | null => {
return jwt.decode(token, { json: true })
}
type GenerateTokenParams = {
payload: Record<string, unknown>
algorithm?: Algorithm
key?: string
keyId?: string
issuer?: string
}
type GenerateMockExternalTokenParams = ExternalPrincipal & {
signingKeyId?: SigningKeyId
privateKey?: string
}
type GenerateMockExternalTokenReturn = {
mockExternalToken: string
mockExternalTokenPayload: ExternalTokenPayload
}

View File

@@ -0,0 +1,95 @@
import { apId, FlowAction, FlowActionType, FlowStatus, FlowTrigger, FlowTriggerType, FlowVersion, FlowVersionState, PopulatedFlow } from '@activepieces/shared'
import { faker } from '@faker-js/faker'
export const flowGenerator = {
simpleActionAndTrigger(externalId?: string): PopulatedFlow {
return flowGenerator.randomizeMetadata(externalId, flowVersionGenerator.simpleActionAndTrigger())
},
randomizeMetadata(externalId: string | undefined, version: Omit<FlowVersion, 'flowId'>): PopulatedFlow {
const flowId = apId()
const result = {
externalId: externalId ?? flowId,
version: {
...version,
trigger: randomizeTriggerMetadata(version.trigger),
flowId,
},
schedule: null,
status: faker.helpers.enumValue(FlowStatus),
id: flowId,
projectId: apId(),
folderId: apId(),
created: faker.date.recent().toISOString(),
updated: faker.date.recent().toISOString(),
}
return result
},
}
const flowVersionGenerator = {
simpleActionAndTrigger(): Omit<FlowVersion, 'flowId'> {
return {
id: apId(),
displayName: faker.animal.dog(),
created: faker.date.recent().toISOString(),
updated: faker.date.recent().toISOString(),
updatedBy: apId(),
valid: true,
trigger: {
...randomizeTriggerMetadata(generateTrigger()),
nextAction: generateAction(),
},
state: FlowVersionState.DRAFT,
connectionIds: [],
agentIds: [],
}
},
}
function randomizeTriggerMetadata(trigger: FlowTrigger): FlowTrigger {
return {
...trigger,
settings: {
...trigger.settings,
propertySettings: {
server: faker.internet.url(),
port: faker.color.cmyk(),
username: faker.internet.userName(),
password: faker.internet.password(),
},
},
}
}
function generateAction(): FlowAction {
return {
type: FlowActionType.PIECE,
displayName: faker.hacker.noun(),
name: apId(),
skip: false,
settings: {
input: {},
pieceName: faker.helpers.arrayElement(['@activepieces/piece-schedule', '@activepieces/piece-webhook']),
pieceVersion: faker.system.semver(),
actionName: faker.hacker.noun(),
propertySettings: {},
},
valid: true,
}
}
function generateTrigger(): FlowTrigger {
return {
type: FlowTriggerType.PIECE,
displayName: faker.hacker.noun(),
name: apId(),
settings: {
pieceName: faker.helpers.arrayElement(['@activepieces/piece-schedule', '@activepieces/piece-webhook']),
pieceVersion: faker.system.semver(),
triggerName: faker.hacker.noun(),
input: {},
propertySettings: {},
},
valid: true,
}
}

View File

@@ -0,0 +1,24 @@
import { SignInRequest, SignUpRequest } from '@activepieces/shared'
import { faker } from '@faker-js/faker'
export const createMockSignUpRequest = (
signUpRequest?: Partial<SignUpRequest>,
): SignUpRequest => {
return {
email: signUpRequest?.email ?? faker.internet.email(),
password: signUpRequest?.password ?? faker.internet.password(),
firstName: signUpRequest?.firstName ?? faker.person.firstName(),
lastName: signUpRequest?.lastName ?? faker.person.lastName(),
trackEvents: signUpRequest?.trackEvents ?? faker.datatype.boolean(),
newsLetter: signUpRequest?.newsLetter ?? faker.datatype.boolean(),
}
}
export const createMockSignInRequest = (
signInRequest?: Partial<SignInRequest>,
): SignInRequest => {
return {
email: signInRequest?.email ?? faker.internet.email(),
password: signInRequest?.password ?? faker.internet.password(),
}
}

View File

@@ -0,0 +1,818 @@
import {
ApiKey,
ApplicationEvent,
ApplicationEventName,
CustomDomain,
CustomDomainStatus,
GitBranchType,
GitRepo,
KeyAlgorithm,
OAuthApp,
OtpModel,
OtpState,
OtpType,
ProjectMember,
SigningKey,
} from '@activepieces/ee-shared'
import { LATEST_CONTEXT_VERSION } from '@activepieces/pieces-framework'
import { apDayjs } from '@activepieces/server-shared'
import { AiOverageState,
AIProvider,
AIProviderName,
apId,
AppConnection,
AppConnectionScope,
AppConnectionStatus,
AppConnectionType,
assertNotNullOrUndefined,
Cell,
ColorName,
Field,
FieldType,
File,
FileCompression,
FileLocation,
FileType,
FilteredPieceBehavior,
Flow,
FlowOperationStatus,
FlowRun,
FlowRunStatus,
FlowStatus,
FlowTriggerType,
FlowVersion,
FlowVersionState,
InvitationStatus,
InvitationType,
PackageType,
PiecesFilterType,
PieceType,
Platform,
PlatformPlan,
PlatformRole,
Project,
ProjectIcon,
ProjectPlan,
ProjectRelease,
ProjectReleaseType,
ProjectRole,
ProjectType,
Record,
RoleType,
RunEnvironment,
Table,
TeamProjectsLimit,
Template,
TemplateStatus,
TemplateType,
User,
UserIdentity,
UserIdentityProvider,
UserInvitation,
UserStatus,
} from '@activepieces/shared'
import { faker } from '@faker-js/faker'
import bcrypt from 'bcrypt'
import dayjs from 'dayjs'
import { AIProviderSchema } from '../../../src/app/ai/ai-provider-entity'
import { databaseConnection } from '../../../src/app/database/database-connection'
import { generateApiKey } from '../../../src/app/ee/api-keys/api-key-service'
import { OAuthAppWithEncryptedSecret } from '../../../src/app/ee/oauth-apps/oauth-app.entity'
import { PlatformPlanEntity } from '../../../src/app/ee/platform/platform-plan/platform-plan.entity'
import { encryptUtils } from '../../../src/app/helper/encryption'
import { PieceMetadataSchema } from '../../../src/app/pieces/metadata/piece-metadata-entity'
import { PieceTagSchema } from '../../../src/app/pieces/tags/pieces/piece-tag.entity'
import { TagEntitySchema } from '../../../src/app/pieces/tags/tag-entity'
export const CLOUD_PLATFORM_ID = 'cloud-id'
export const createMockUserIdentity = (userIdentity?: Partial<UserIdentity>): UserIdentity => {
return {
id: userIdentity?.id ?? apId(),
created: userIdentity?.created ?? faker.date.recent().toISOString(),
updated: userIdentity?.updated ?? faker.date.recent().toISOString(),
email: (userIdentity?.email ?? faker.internet.email()).toLowerCase().trim(),
firstName: userIdentity?.firstName ?? faker.person.firstName(),
lastName: userIdentity?.lastName ?? faker.person.lastName(),
tokenVersion: userIdentity?.tokenVersion ?? undefined,
password: userIdentity?.password
? bcrypt.hashSync(userIdentity.password, 10)
: faker.internet.password(),
trackEvents: userIdentity?.trackEvents ?? faker.datatype.boolean(),
newsLetter: userIdentity?.newsLetter ?? faker.datatype.boolean(),
verified: userIdentity?.verified ?? faker.datatype.boolean(),
provider: userIdentity?.provider ?? UserIdentityProvider.EMAIL,
}
}
export const createMockUser = (user?: Partial<User>): User => {
return {
id: user?.id ?? apId(),
created: user?.created ?? faker.date.recent().toISOString(),
updated: user?.updated ?? faker.date.recent().toISOString(),
status: user?.status ?? UserStatus.ACTIVE,
platformRole: user?.platformRole ?? faker.helpers.enumValue(PlatformRole),
externalId: user?.externalId,
identityId: user?.identityId ?? apId(),
platformId: user?.platformId ?? null,
}
}
export const createMockOAuthApp = async (
oAuthApp?: Partial<OAuthApp>,
): Promise<OAuthAppWithEncryptedSecret> => {
return {
id: oAuthApp?.id ?? apId(),
created: oAuthApp?.created ?? faker.date.recent().toISOString(),
updated: oAuthApp?.updated ?? faker.date.recent().toISOString(),
platformId: oAuthApp?.platformId ?? apId(),
pieceName: oAuthApp?.pieceName ?? faker.lorem.word(),
clientId: oAuthApp?.clientId ?? apId(),
clientSecret: await encryptUtils.encryptString(faker.lorem.word()),
}
}
export const createMockTemplate = (
template?: Partial<Template>,
): Template => {
return {
id: template?.id ?? apId(),
created: template?.created ?? faker.date.recent().toISOString(),
updated: template?.updated ?? faker.date.recent().toISOString(),
pieces: template?.pieces ?? [],
flows: template?.flows ?? [createMockFlowVersion()],
platformId: template?.platformId ?? apId(),
name: template?.name ?? faker.lorem.word(),
type: template?.type ?? TemplateType.CUSTOM,
description: template?.description ?? faker.lorem.sentence(),
summary: template?.summary ?? faker.lorem.sentence(),
tags: template?.tags ?? [],
blogUrl: template?.blogUrl ?? faker.internet.url(),
metadata: template?.metadata ?? null,
usageCount: template?.usageCount ?? 0,
author: template?.author ?? faker.person.fullName(),
categories: template?.categories ?? [],
status: template?.status ?? TemplateStatus.PUBLISHED,
}
}
export const createMockPlan = (plan?: Partial<ProjectPlan>): ProjectPlan => {
return {
id: plan?.id ?? apId(),
created: plan?.created ?? faker.date.recent().toISOString(),
updated: plan?.updated ?? faker.date.recent().toISOString(),
projectId: plan?.projectId ?? apId(),
name: plan?.name ?? faker.lorem.word(),
locked: plan?.locked ?? false,
pieces: plan?.pieces ?? [],
piecesFilterType: plan?.piecesFilterType ?? PiecesFilterType.NONE,
}
}
export const createMockUserInvitation = (userInvitation: Partial<UserInvitation>): UserInvitation => {
return {
id: userInvitation.id ?? apId(),
created: userInvitation.created ?? faker.date.recent().toISOString(),
updated: userInvitation.updated ?? faker.date.recent().toISOString(),
email: userInvitation.email ?? faker.internet.email(),
type: userInvitation.type ?? faker.helpers.enumValue(InvitationType),
platformId: userInvitation.platformId ?? apId(),
projectId: userInvitation.projectId,
projectRole: userInvitation.projectRole,
platformRole: userInvitation.platformRole,
status: userInvitation.status ?? faker.helpers.enumValue(InvitationStatus),
}
}
export const createMockProject = (project?: Partial<Project>): Project => {
const icon: ProjectIcon = {
color: faker.helpers.enumValue(ColorName),
}
return {
id: project?.id ?? apId(),
created: project?.created ?? faker.date.recent().toISOString(),
updated: project?.updated ?? faker.date.recent().toISOString(),
deleted: project?.deleted ?? null,
ownerId: project?.ownerId ?? apId(),
displayName: project?.displayName ?? faker.lorem.word(),
platformId: project?.platformId ?? apId(),
externalId: project?.externalId ?? apId(),
releasesEnabled: project?.releasesEnabled ?? false,
metadata: project?.metadata ?? null,
type: project?.type ?? ProjectType.TEAM,
icon,
}
}
export const createMockGitRepo = (gitRepo?: Partial<GitRepo>): GitRepo => {
return {
id: gitRepo?.id ?? apId(),
branchType: faker.helpers.enumValue(GitBranchType),
created: gitRepo?.created ?? faker.date.recent().toISOString(),
updated: gitRepo?.updated ?? faker.date.recent().toISOString(),
projectId: gitRepo?.projectId ?? apId(),
remoteUrl: gitRepo?.remoteUrl ?? `git@${faker.internet.url()}`,
sshPrivateKey: gitRepo?.sshPrivateKey ?? faker.internet.password(),
branch: gitRepo?.branch ?? faker.lorem.word(),
slug: gitRepo?.slug ?? faker.lorem.word(),
}
}
export const createMockPlatformPlan = (platformPlan?: Partial<PlatformPlan>): PlatformPlan => {
return {
id: platformPlan?.id ?? apId(),
created: platformPlan?.created ?? faker.date.recent().toISOString(),
updated: platformPlan?.updated ?? faker.date.recent().toISOString(),
platformId: platformPlan?.platformId ?? apId(),
includedAiCredits: platformPlan?.includedAiCredits ?? 0,
licenseKey: platformPlan?.licenseKey ?? faker.lorem.word(),
stripeCustomerId: undefined,
mcpsEnabled: platformPlan?.mcpsEnabled ?? false,
stripeSubscriptionId: undefined,
ssoEnabled: platformPlan?.ssoEnabled ?? false,
agentsEnabled: platformPlan?.agentsEnabled ?? false,
aiCreditsOverageLimit: platformPlan?.aiCreditsOverageLimit ?? 0,
aiCreditsOverageState: platformPlan?.aiCreditsOverageState ?? AiOverageState.ALLOWED_BUT_OFF,
environmentsEnabled: platformPlan?.environmentsEnabled ?? false,
analyticsEnabled: platformPlan?.analyticsEnabled ?? false,
auditLogEnabled: platformPlan?.auditLogEnabled ?? false,
globalConnectionsEnabled: platformPlan?.globalConnectionsEnabled ?? false,
customRolesEnabled: platformPlan?.customRolesEnabled ?? false,
managePiecesEnabled: platformPlan?.managePiecesEnabled ?? false,
manageTemplatesEnabled: platformPlan?.manageTemplatesEnabled ?? false,
customAppearanceEnabled: platformPlan?.customAppearanceEnabled ?? false,
apiKeysEnabled: platformPlan?.apiKeysEnabled ?? false,
stripeSubscriptionStatus: undefined,
showPoweredBy: platformPlan?.showPoweredBy ?? false,
embeddingEnabled: platformPlan?.embeddingEnabled ?? false,
teamProjectsLimit: platformPlan?.teamProjectsLimit ?? TeamProjectsLimit.NONE,
projectRolesEnabled: platformPlan?.projectRolesEnabled ?? false,
customDomainsEnabled: platformPlan?.customDomainsEnabled ?? false,
tablesEnabled: platformPlan?.tablesEnabled ?? false,
todosEnabled: platformPlan?.todosEnabled ?? false,
stripeSubscriptionEndDate: apDayjs().endOf('month').unix(),
stripeSubscriptionStartDate: apDayjs().startOf('month').unix(),
plan: platformPlan?.plan,
}
}
export const createMockPlatform = (platform?: Partial<Platform>): Platform => {
return {
id: platform?.id ?? apId(),
created: platform?.created ?? faker.date.recent().toISOString(),
updated: platform?.updated ?? faker.date.recent().toISOString(),
ownerId: platform?.ownerId ?? apId(),
enforceAllowedAuthDomains: platform?.enforceAllowedAuthDomains ?? false,
federatedAuthProviders: platform?.federatedAuthProviders ?? {},
allowedAuthDomains: platform?.allowedAuthDomains ?? [],
name: platform?.name ?? faker.lorem.word(),
primaryColor: platform?.primaryColor ?? faker.color.rgb(),
logoIconUrl: platform?.logoIconUrl ?? faker.image.urlPlaceholder(),
fullLogoUrl: platform?.fullLogoUrl ?? faker.image.urlPlaceholder(),
emailAuthEnabled: platform?.emailAuthEnabled ?? faker.datatype.boolean(),
pinnedPieces: platform?.pinnedPieces ?? [],
favIconUrl: platform?.favIconUrl ?? faker.image.urlPlaceholder(),
filteredPieceNames: platform?.filteredPieceNames ?? [],
filteredPieceBehavior:
platform?.filteredPieceBehavior ??
faker.helpers.enumValue(FilteredPieceBehavior),
cloudAuthEnabled: platform?.cloudAuthEnabled ?? faker.datatype.boolean(),
}
}
export const createMockPlatformWithOwner = (
params?: CreateMockPlatformWithOwnerParams,
): CreateMockPlatformWithOwnerReturn => {
const mockOwnerId = params?.owner?.id ?? apId()
const mockPlatformId = params?.platform?.id ?? apId()
const mockUserIdentity = createMockUserIdentity({})
const mockOwner = createMockUser({
identityId: mockUserIdentity.id,
...params?.owner,
id: mockOwnerId,
platformId: mockPlatformId,
platformRole: PlatformRole.ADMIN,
})
const mockPlatform = createMockPlatform({
...params?.platform,
id: mockPlatformId,
ownerId: mockOwnerId,
})
return {
mockUserIdentity,
mockPlatform,
mockOwner,
}
}
export const createMockProjectMember = (
projectMember?: Omit<Partial<ProjectMember>, 'projectRoleId'> & {
projectRoleId: string
},
): ProjectMember => {
assertNotNullOrUndefined(projectMember?.userId, 'userId')
return {
id: projectMember?.id ?? apId(),
created: projectMember?.created ?? faker.date.recent().toISOString(),
updated: projectMember?.updated ?? faker.date.recent().toISOString(),
platformId: projectMember?.platformId ?? apId(),
projectRoleId: projectMember.projectRoleId,
userId: projectMember?.userId,
projectId: projectMember?.projectId ?? apId(),
}
}
const MOCK_SIGNING_KEY_PUBLIC_KEY = `-----BEGIN RSA PUBLIC KEY-----
MIICCgKCAgEAlnd5vGP/1bzcndN/yRD+ZTd6tuemxaJd+12bOZ2QCXcTM03AKSp3
NE5QMyIi13PXMg+z1uPowfivPJ4iVTMaW1U00O7JlUduGR0VrG0BCJlfEf852V71
TfE+2+EpMme9Yw6Gs/YAuOwgVwu3n/XF0il3FTIm1oY1a/MA79rv0RSscnIgCaYJ
e86LWm+H6753Si0MIId/ajIfYYIndN6qRIlPsgagdL+kljUSPEiIzmV0POxTltBo
tXL1t7Mu+meJrY85MXG5W8BS05+q6dJql7Cl0UbPK152ziakB+biMI/4hYlaOIBT
3KeOcz/Jg7Zv21Y0tbdrZ5osVrrNpFsCV7PGyQIUDVmmnCHrOEBS2XM5zOHzTxMl
JQh3Db318rB5415zuBTzrO+20++03kH4SwZEEBg1SDAInYwLOWldbTuZuD0Hx7P2
g4a3OqHHVOcAgtsHgmU7/zCgCIETg4KbRdpSsqOm/YJDWWoLDTwvKnH5QHSBacq1
kxbNAUSuLQESkfZq1Dw5+tdBDJr29bxjmiSggyittTYn1B3iHACNoe4zj9sMQQIf
j9mmntXsa/leIwBVspiEOHYZwJOe5+goSd8K1VIQJxC1DVBxB2eHxMvuo3eyJ0HE
DlebIeZy4zrE1LPgRic1kfdemyxvuN3iwZnPGiY79nL1ZNDM3M4ApSMCAwEAAQ==
-----END RSA PUBLIC KEY-----`
export const createMockApiKey = (
apiKey?: Partial<Omit<ApiKey, 'hashedValue' | 'truncatedValue'>>,
): ApiKey & { value: string } => {
const { secretHashed, secretTruncated, secret } = generateApiKey()
return {
id: apiKey?.id ?? apId(),
created: apiKey?.created ?? faker.date.recent().toISOString(),
updated: apiKey?.updated ?? faker.date.recent().toISOString(),
displayName: apiKey?.displayName ?? faker.lorem.word(),
platformId: apiKey?.platformId ?? apId(),
hashedValue: secretHashed,
value: secret,
truncatedValue: secretTruncated,
}
}
export const createMockSigningKey = (
signingKey?: Partial<SigningKey>,
): SigningKey => {
return {
id: signingKey?.id ?? apId(),
created: signingKey?.created ?? faker.date.recent().toISOString(),
updated: signingKey?.updated ?? faker.date.recent().toISOString(),
displayName: signingKey?.displayName ?? faker.lorem.word(),
platformId: signingKey?.platformId ?? apId(),
publicKey: signingKey?.publicKey ?? MOCK_SIGNING_KEY_PUBLIC_KEY,
algorithm: signingKey?.algorithm ?? KeyAlgorithm.RSA,
}
}
export const createMockTag = (tag?: Partial<Omit<TagEntitySchema, 'platform'>>): Omit<TagEntitySchema, 'platform'> => {
return {
id: tag?.id ?? apId(),
created: tag?.created ?? faker.date.recent().toISOString(),
updated: tag?.updated ?? faker.date.recent().toISOString(),
platformId: tag?.platformId ?? apId(),
name: tag?.name ?? faker.lorem.word(),
}
}
export const createMockPieceTag = (request: Partial<Omit<PieceTagSchema, 'platform' | 'tag'>>): Omit<PieceTagSchema, 'platform' | 'tag'> => {
return {
id: request.id ?? apId(),
created: request.created ?? faker.date.recent().toISOString(),
updated: request.updated ?? faker.date.recent().toISOString(),
platformId: request.platformId ?? apId(),
pieceName: request.pieceName ?? faker.lorem.word(),
tagId: request.tagId ?? apId(),
}
}
export const createMockPieceMetadata = (
pieceMetadata?: Partial<Omit<PieceMetadataSchema, 'project'>>,
): Omit<PieceMetadataSchema, 'project'> => {
return {
id: pieceMetadata?.id ?? apId(),
projectUsage: 0,
created: pieceMetadata?.created ?? faker.date.recent().toISOString(),
updated: pieceMetadata?.updated ?? faker.date.recent().toISOString(),
name: pieceMetadata?.name ?? faker.lorem.word(),
displayName: pieceMetadata?.displayName ?? faker.lorem.word(),
logoUrl: pieceMetadata?.logoUrl ?? faker.image.urlPlaceholder(),
description: pieceMetadata?.description ?? faker.lorem.sentence(),
projectId: pieceMetadata?.projectId,
directoryPath: pieceMetadata?.directoryPath,
auth: pieceMetadata?.auth,
authors: pieceMetadata?.authors ?? [],
platformId: pieceMetadata?.platformId,
version: pieceMetadata?.version ?? faker.system.semver(),
minimumSupportedRelease: pieceMetadata?.minimumSupportedRelease ?? '0.0.0',
maximumSupportedRelease: pieceMetadata?.maximumSupportedRelease ?? '9.9.9',
actions: pieceMetadata?.actions ?? {},
triggers: pieceMetadata?.triggers ?? {},
pieceType: pieceMetadata?.pieceType ?? faker.helpers.enumValue(PieceType),
packageType:
pieceMetadata?.packageType ?? faker.helpers.enumValue(PackageType),
archiveId: pieceMetadata?.archiveId,
categories: pieceMetadata?.categories ?? [],
contextInfo: pieceMetadata?.contextInfo ?? { version: LATEST_CONTEXT_VERSION },
}
}
export const createAuditEvent = (auditEvent: Partial<ApplicationEvent>) => {
return {
id: auditEvent.id ?? apId(),
created: auditEvent.created ?? faker.date.recent().toISOString(),
updated: auditEvent.updated ?? faker.date.recent().toISOString(),
ip: auditEvent.ip ?? faker.internet.ip(),
platformId: auditEvent.platformId,
userId: auditEvent.userId,
userEmail: auditEvent.userEmail ?? faker.internet.email(),
action: auditEvent.action ?? faker.helpers.enumValue(ApplicationEventName),
data: auditEvent.data ?? {},
}
}
export const createMockCustomDomain = (
customDomain?: Partial<CustomDomain>,
): CustomDomain => {
return {
id: customDomain?.id ?? apId(),
created: customDomain?.created ?? faker.date.recent().toISOString(),
updated: customDomain?.updated ?? faker.date.recent().toISOString(),
domain: customDomain?.domain ?? faker.internet.domainName(),
platformId: customDomain?.platformId ?? apId(),
status: customDomain?.status ?? faker.helpers.enumValue(CustomDomainStatus),
}
}
export const createMockOtp = (otp?: Partial<OtpModel>): OtpModel => {
const now = dayjs()
const twentyMinutesAgo = now.subtract(5, 'minutes')
return {
id: otp?.id ?? apId(),
created: otp?.created ?? faker.date.recent().toISOString(),
updated:
otp?.updated ??
faker.date
.between({ from: twentyMinutesAgo.toDate(), to: now.toDate() })
.toISOString(),
type: otp?.type ?? faker.helpers.enumValue(OtpType),
identityId: otp?.identityId ?? apId(),
value:
otp?.value ?? faker.number.int({ min: 100000, max: 999999 }).toString(),
state: otp?.state ?? faker.helpers.enumValue(OtpState),
}
}
export const createMockFlowRun = (flowRun?: Partial<FlowRun>): FlowRun => {
return {
id: flowRun?.id ?? apId(),
created: flowRun?.created ?? faker.date.recent().toISOString(),
updated: flowRun?.updated ?? faker.date.recent().toISOString(),
projectId: flowRun?.projectId ?? apId(),
flowId: flowRun?.flowId ?? apId(),
tags: flowRun?.tags ?? [],
steps: {},
failParentOnFailure: flowRun?.failParentOnFailure ?? false,
parentRunId: flowRun?.parentRunId ?? undefined,
flowVersionId: flowRun?.flowVersionId ?? apId(),
flowVersion: flowRun?.flowVersion,
logsFileId: flowRun?.logsFileId ?? null,
status: flowRun?.status ?? faker.helpers.enumValue(FlowRunStatus),
startTime: flowRun?.startTime ?? faker.date.recent().toISOString(),
finishTime: flowRun?.finishTime ?? faker.date.recent().toISOString(),
environment:
flowRun?.environment ?? faker.helpers.enumValue(RunEnvironment),
}
}
export const createMockFlow = (flow?: Partial<Flow>): Flow => {
return {
id: flow?.id ?? apId(),
created: flow?.created ?? faker.date.recent().toISOString(),
updated: flow?.updated ?? faker.date.recent().toISOString(),
projectId: flow?.projectId ?? apId(),
status: flow?.status ?? faker.helpers.enumValue(FlowStatus),
folderId: flow?.folderId ?? null,
operationStatus: flow?.operationStatus ?? FlowOperationStatus.NONE,
publishedVersionId: flow?.publishedVersionId ?? null,
externalId: flow?.externalId ?? apId(),
}
}
export const createMockFlowVersion = (
flowVersion?: Partial<FlowVersion>,
): FlowVersion => {
const emptyTrigger = {
type: FlowTriggerType.EMPTY,
name: 'trigger',
settings: {},
valid: false,
displayName: 'Select Trigger',
} as const
return {
id: flowVersion?.id ?? apId(),
created: flowVersion?.created ?? faker.date.recent().toISOString(),
updated: flowVersion?.updated ?? faker.date.recent().toISOString(),
displayName: flowVersion?.displayName ?? faker.word.words(),
flowId: flowVersion?.flowId ?? apId(),
agentIds: flowVersion?.agentIds ?? [],
trigger: flowVersion?.trigger ?? emptyTrigger,
connectionIds: flowVersion?.connectionIds ?? [],
state: flowVersion?.state ?? faker.helpers.enumValue(FlowVersionState),
updatedBy: flowVersion?.updatedBy,
valid: flowVersion?.valid ?? faker.datatype.boolean(),
}
}
export const createMockConnection = (connection: Partial<AppConnection>, ownerId: string): AppConnection<AppConnectionType.SECRET_TEXT> => {
return {
id: connection?.id ?? apId(),
created: connection?.created ?? faker.date.recent().toISOString(),
updated: connection?.updated ?? faker.date.recent().toISOString(),
platformId: connection?.platformId ?? apId(),
projectIds: connection?.projectIds ?? [],
pieceName: connection?.pieceName ?? faker.lorem.word(),
displayName: connection?.displayName ?? faker.lorem.word(),
type: AppConnectionType.SECRET_TEXT,
scope: AppConnectionScope.PROJECT,
status: AppConnectionStatus.ACTIVE,
ownerId,
value: {
type: AppConnectionType.SECRET_TEXT,
secret_text: faker.lorem.word(),
},
metadata: connection?.metadata ?? {},
externalId: connection?.externalId ?? apId(),
owner: null,
pieceVersion: connection?.pieceVersion ?? '0.0.0',
}
}
const createMockTable = ({ projectId }: { projectId: string }): Table => {
return {
id: apId(),
created: faker.date.recent().toISOString(),
updated: faker.date.recent().toISOString(),
projectId,
externalId: apId(),
name: faker.lorem.word(),
}
}
const createMockField = ({ tableId, projectId }: { tableId: string, projectId: string }): Field => {
return {
id: apId(),
created: faker.date.recent().toISOString(),
updated: faker.date.recent().toISOString(),
tableId,
name: faker.lorem.word(),
data: {
options: [],
},
externalId: apId(),
projectId,
type: FieldType.STATIC_DROPDOWN,
}
}
const createMockRecord = ({ tableId, projectId }: { tableId: string, projectId: string }): Record => {
return {
id: apId(),
created: faker.date.recent().toISOString(),
updated: faker.date.recent().toISOString(),
tableId,
projectId,
}
}
const createMockCell = ({ recordId, fieldId, projectId }: { recordId: string, fieldId: string, projectId: string }): Cell => {
return {
id: apId(),
created: faker.date.recent().toISOString(),
updated: faker.date.recent().toISOString(),
recordId,
fieldId,
projectId,
value: faker.lorem.word(),
}
}
type Solution = {
table: Table
connection: AppConnection<AppConnectionType.SECRET_TEXT>
flow: Flow
flowRun: FlowRun
flowVersion: FlowVersion
cell: Cell
}
export const createMockSolutionAndSave = async ({ projectId, platformId, userId }: { projectId: string, platformId: string, userId: string }): Promise<Solution> => {
const table = createMockTable({ projectId })
const field = createMockField({ tableId: table.id, projectId })
const record = createMockRecord({ tableId: table.id, projectId })
const cell = createMockCell({ recordId: record.id, fieldId: field.id, projectId })
const connection = createMockConnection({ projectIds: [projectId], platformId }, userId)
const flow = createMockFlow({ projectId })
const flowVersion = createMockFlowVersion({ flowId: flow.id })
const flowRun = createMockFlowRun({ projectId, flowId: flow.id, flowVersionId: flowVersion.id })
await databaseConnection().getRepository('table').save([table])
await databaseConnection().getRepository('field').save([field])
await databaseConnection().getRepository('record').save([record])
await databaseConnection().getRepository('cell').save([cell])
await databaseConnection().getRepository('app_connection').save([connection])
await databaseConnection().getRepository('flow').save([flow])
await databaseConnection().getRepository('flow_version').save([flowVersion])
await databaseConnection().getRepository('flow_run').save([flowRun])
return { table, connection, flow, flowRun, flowVersion, cell }
}
export const checkIfSolutionExistsInDb = async (solution: Solution): Promise<boolean> => {
const table = await databaseConnection().getRepository('table').findOneBy({ id: solution.table.id })
const connection = await databaseConnection().getRepository('app_connection').findOneBy({ id: solution.connection.id })
const flow = await databaseConnection().getRepository('flow').findOneBy({ id: solution.flow.id })
const flowRun = await databaseConnection().getRepository('flow_run').findOneBy({ id: solution.flowRun.id })
const flowVersion = await databaseConnection().getRepository('flow_version').findOneBy({ id: solution.flowVersion.id })
const cell = await databaseConnection().getRepository('cell').findOneBy({ id: solution.cell.id })
return table !== null && connection !== null && flow !== null && flowRun !== null && flowVersion !== null && cell !== null
}
export const mockBasicUser = async ({ userIdentity, user }: { userIdentity?: Partial<UserIdentity>, user?: Partial<User> }) => {
const mockUserIdentity = createMockUserIdentity({
verified: true,
...userIdentity,
})
await databaseConnection().getRepository('user_identity').save(mockUserIdentity)
const mockUser = createMockUser({
...user,
identityId: mockUserIdentity.id,
})
await databaseConnection().getRepository('user').save(mockUser)
return {
mockUserIdentity,
mockUser,
}
}
export const mockAndSaveBasicSetup = async (params?: MockBasicSetupParams): Promise<MockBasicSetup> => {
const mockUserIdentity = createMockUserIdentity({
verified: true,
...params?.userIdentity,
})
await databaseConnection().getRepository('user_identity').save(mockUserIdentity)
const mockOwner = createMockUser({
...params?.user,
identityId: mockUserIdentity.id,
platformRole: PlatformRole.ADMIN,
})
await databaseConnection().getRepository('user').save(mockOwner)
const mockPlatform = createMockPlatform({
...params?.platform,
ownerId: mockOwner.id,
filteredPieceBehavior: params?.platform?.filteredPieceBehavior ?? FilteredPieceBehavior.BLOCKED,
})
await databaseConnection().getRepository('platform').save(mockPlatform)
const hasPlanTable = databaseConnection().hasMetadata(PlatformPlanEntity)
if (hasPlanTable) {
const mockPlatformPlan = createMockPlatformPlan({
platformId: mockPlatform.id,
auditLogEnabled: true,
apiKeysEnabled: true,
customRolesEnabled: true,
teamProjectsLimit: TeamProjectsLimit.UNLIMITED,
customDomainsEnabled: true,
includedAiCredits: 1000,
...params?.plan,
})
await databaseConnection().getRepository('platform_plan').upsert(mockPlatformPlan, ['platformId'])
}
mockOwner.platformId = mockPlatform.id
await databaseConnection().getRepository('user').save(mockOwner)
const mockProject = createMockProject({
...params?.project,
ownerId: mockOwner.id,
platformId: mockPlatform.id,
})
await databaseConnection().getRepository('project').save(mockProject)
return {
mockUserIdentity,
mockOwner,
mockPlatform,
mockProject,
}
}
type MockBasicSetupWithApiKey = MockBasicSetup & { mockApiKey: ApiKey & { value: string } }
export const mockAndSaveBasicSetupWithApiKey = async (params?: MockBasicSetupParams): Promise<MockBasicSetupWithApiKey> => {
const basicSetup = await mockAndSaveBasicSetup(params)
const mockApiKey = createMockApiKey({
platformId: basicSetup.mockPlatform.id,
})
await databaseConnection().getRepository('api_key').save(mockApiKey)
return {
...basicSetup,
mockApiKey,
}
}
export const createMockFile = (file?: Partial<File>): File => {
return {
id: file?.id ?? apId(),
created: file?.created ?? faker.date.recent().toISOString(),
updated: file?.updated ?? faker.date.recent().toISOString(),
platformId: file?.platformId ?? apId(),
projectId: file?.projectId ?? apId(),
location: file?.location ?? FileLocation.DB,
compression: file?.compression ?? faker.helpers.enumValue(FileCompression),
data: file?.data ?? Buffer.from(faker.lorem.paragraphs()),
type: file?.type ?? faker.helpers.enumValue(FileType),
}
}
export const createMockProjectRole = (projectRole?: Partial<ProjectRole>): ProjectRole => {
return {
id: projectRole?.id ?? apId(),
name: projectRole?.name ?? faker.lorem.word(),
created: projectRole?.created ?? faker.date.recent().toISOString(),
updated: projectRole?.updated ?? faker.date.recent().toISOString(),
permissions: projectRole?.permissions ?? [],
platformId: projectRole?.platformId ?? apId(),
type: projectRole?.type ?? faker.helpers.enumValue(RoleType),
}
}
export const createMockProjectRelease = (projectRelease?: Partial<ProjectRelease>): ProjectRelease => {
return {
id: projectRelease?.id ?? apId(),
created: projectRelease?.created ?? faker.date.recent().toISOString(),
updated: projectRelease?.updated ?? faker.date.recent().toISOString(),
projectId: projectRelease?.projectId ?? apId(),
importedBy: projectRelease?.importedBy ?? apId(),
fileId: projectRelease?.fileId ?? apId(),
name: projectRelease?.name ?? faker.lorem.word(),
description: projectRelease?.description ?? faker.lorem.sentence(),
type: projectRelease?.type ?? faker.helpers.enumValue(ProjectReleaseType),
}
}
export const createMockAIProvider = async (aiProvider?: Partial<AIProvider>): Promise<Omit<AIProviderSchema, 'platform'>> => {
return {
id: aiProvider?.id ?? apId(),
created: aiProvider?.created ?? faker.date.recent().toISOString(),
updated: aiProvider?.updated ?? faker.date.recent().toISOString(),
platformId: aiProvider?.platformId ?? apId(),
provider: aiProvider?.provider ?? faker.helpers.enumValue(AIProviderName),
displayName: aiProvider?.displayName ?? faker.lorem.word(),
config: await encryptUtils.encryptObject({
apiKey: aiProvider?.config?.apiKey ?? process.env.OPENAI_API_KEY ?? faker.string.uuid(),
}),
}
}
export const mockAndSaveAIProvider = async (params?: Partial<AIProvider>): Promise<Omit<AIProviderSchema, 'platform'>> => {
const mockAIProvider = await createMockAIProvider(params)
await databaseConnection().getRepository('ai_provider').upsert(mockAIProvider, ['platformId', 'provider'])
return mockAIProvider
}
type CreateMockPlatformWithOwnerParams = {
platform?: Partial<Omit<Platform, 'ownerId'>>
owner?: Partial<Omit<User, 'platformId'>>
}
type CreateMockPlatformWithOwnerReturn = {
mockPlatform: Platform
mockOwner: User
mockUserIdentity: UserIdentity
}
type MockBasicSetup = {
mockOwner: User
mockPlatform: Platform
mockProject: Project
mockUserIdentity: UserIdentity
}
type MockBasicSetupParams = {
userIdentity?: Partial<UserIdentity>
user?: Partial<User>
plan?: Partial<PlatformPlan>
platform?: Partial<Platform>
project?: Partial<Project>
}

View File

@@ -0,0 +1,47 @@
import { apId, Field, FieldState, FieldType, PopulatedTable, TableAutomationStatus } from '@activepieces/shared'
import { faker } from '@faker-js/faker'
export const tableGenerator = {
simpleTable(table: Partial<PopulatedTable>): PopulatedTable {
const tableId = apId()
return {
id: tableId,
name: faker.lorem.word(),
externalId: table.externalId ?? apId(),
fields: table.fields ?? [
tableGenerator.generateRandomField(tableId),
tableGenerator.generateRandomField(tableId),
],
projectId: apId(),
created: faker.date.recent().toISOString(),
updated: faker.date.recent().toISOString(),
status: table.status ?? TableAutomationStatus.ENABLED,
trigger: table.trigger ?? null,
}
},
generateRandomField(tableId: string): Field {
return {
id: apId(),
projectId: apId(),
created: faker.date.recent().toISOString(),
updated: faker.date.recent().toISOString(),
tableId,
name: faker.lorem.word(),
type: FieldType.TEXT,
externalId: apId(),
}
},
generateRandomDropdownField(): FieldState {
return {
name: faker.lorem.word(),
type: FieldType.STATIC_DROPDOWN,
externalId: apId(),
data: {
options: [
{ value: faker.lorem.word() },
{ value: faker.lorem.word() },
],
},
}
},
}

View File

@@ -0,0 +1,165 @@
import { FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
import {
createMockSignInRequest,
createMockSignUpRequest,
} from '../../../helpers/mocks/authn'
let app: FastifyInstance | null = null
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
beforeEach(async () => {
await databaseConnection().getRepository('flag').createQueryBuilder().delete().execute()
await databaseConnection().getRepository('project').createQueryBuilder().delete().execute()
await databaseConnection().getRepository('platform').createQueryBuilder().delete().execute()
await databaseConnection().getRepository('user').createQueryBuilder().delete().execute()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('Authentication API', () => {
describe('Sign up Endpoint', () => {
it('Adds new user', async () => {
// arrange
const mockSignUpRequest = createMockSignUpRequest()
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/authentication/sign-up',
body: mockSignUpRequest,
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody?.id).toHaveLength(21)
expect(responseBody?.created).toBeDefined()
expect(responseBody?.updated).toBeDefined()
expect(responseBody?.verified).toBe(true)
expect(responseBody?.email).toBe(mockSignUpRequest.email.toLocaleLowerCase().trim())
expect(responseBody?.firstName).toBe(mockSignUpRequest.firstName)
expect(responseBody?.lastName).toBe(mockSignUpRequest.lastName)
expect(responseBody?.trackEvents).toBe(mockSignUpRequest.trackEvents)
expect(responseBody?.newsLetter).toBe(mockSignUpRequest.newsLetter)
expect(responseBody?.status).toBe('ACTIVE')
expect(responseBody?.platformId).toBeDefined()
expect(responseBody?.externalId).toBe(null)
expect(responseBody?.projectId).toHaveLength(21)
expect(responseBody?.token).toBeDefined()
})
it('Creates new project for user', async () => {
// arrange
const mockSignUpRequest = createMockSignUpRequest()
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/authentication/sign-up',
body: mockSignUpRequest,
})
const responseBody = response?.json()
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
const project = await databaseConnection()
.getRepository('project')
.findOneBy({
id: responseBody.projectId,
})
expect(project?.ownerId).toBe(responseBody.id)
expect(project?.displayName).toBeDefined()
expect(project?.platformId).toBeDefined()
})
})
describe('Sign in Endpoint', () => {
it('Logs in existing users', async () => {
// arrange
const mockSignUpRequest = createMockSignUpRequest()
// First sign up the user
const signUpResponse = await app?.inject({
method: 'POST',
url: '/v1/authentication/sign-up',
body: mockSignUpRequest,
})
const signUpBody = signUpResponse?.json()
// Then try to sign in
const mockSignInRequest = createMockSignInRequest({
email: mockSignUpRequest.email,
password: mockSignUpRequest.password,
})
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/authentication/sign-in',
body: mockSignInRequest,
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody?.id).toBe(signUpBody.id)
expect(responseBody?.email).toBe(mockSignUpRequest.email.toLowerCase().trim())
expect(responseBody?.firstName).toBe(mockSignUpRequest.firstName)
expect(responseBody?.lastName).toBe(mockSignUpRequest.lastName)
expect(responseBody?.trackEvents).toBe(mockSignUpRequest.trackEvents)
expect(responseBody?.newsLetter).toBe(mockSignUpRequest.newsLetter)
expect(responseBody?.password).toBeUndefined()
expect(responseBody?.status).toBe('ACTIVE')
expect(responseBody?.verified).toBe(true)
expect(responseBody?.platformId).toBe(signUpBody.platformId)
expect(responseBody?.externalId).toBe(null)
expect(responseBody?.projectId).toBe(signUpBody.projectId)
expect(responseBody?.token).toBeDefined()
})
it('Fails if password doesn\'t match', async () => {
// arrange
const mockSignUpRequest = createMockSignUpRequest()
// First sign up the user
await app?.inject({
method: 'POST',
url: '/v1/authentication/sign-up',
body: mockSignUpRequest,
})
const mockSignInRequest = createMockSignInRequest({
email: mockSignUpRequest.email,
password: 'wrong password',
})
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/authentication/sign-in',
body: mockSignInRequest,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.UNAUTHORIZED)
const responseBody = response?.json()
expect(responseBody?.code).toBe('INVALID_CREDENTIALS')
})
})
})

View File

@@ -0,0 +1,94 @@
import bcrypt from 'bcrypt'
import { passwordHasher } from '../../../../src/app/authentication/lib/password-hasher'
const SCRYPT_SEPARATOR = '~'
describe('Password Hasher', () => {
const plainTextPassword = 'password123'
describe('hash', () => {
it('should not produce the same hash for the same password', async () => {
const hashedPassword1 = await bcrypt.hash(plainTextPassword, 10)
const hashedPassword2 = await bcrypt.hash(plainTextPassword, 10)
expect(hashedPassword1).not.toBe(hashedPassword2)
})
it('should verify hashed password correctly', async () => {
const hashedPassword = await bcrypt.hash(plainTextPassword, 10)
const result = await bcrypt.compare(plainTextPassword, hashedPassword)
expect(result).toBe(true)
})
it('should fail to verify incorrect password', async () => {
const hashedPassword = await bcrypt.hash(plainTextPassword, 10)
const incorrectPassword = 'incorrectPassword'
const result = await bcrypt.compare(incorrectPassword, hashedPassword)
expect(result).toBe(false)
})
})
describe('compare', () => {
it('should return true for identical bcrypt passwords', async () => {
const hashedPassword = await bcrypt.hash(plainTextPassword, 10)
const result = await passwordHasher.compare(
plainTextPassword,
hashedPassword,
)
expect(result).toBe(true)
})
it('should return false for different bcrypt passwords', async () => {
const hashedPassword = await bcrypt.hash(plainTextPassword, 10)
const differentPassword = 'differentPassword'
const result = await passwordHasher.compare(
differentPassword,
hashedPassword,
)
expect(result).toBe(false)
})
it('should return false for empty password bcrypt comparison', async () => {
const hashedPassword = await bcrypt.hash(plainTextPassword, 10)
const result = await passwordHasher.compare('', hashedPassword)
expect(result).toBe(false)
})
it('should return false for empty hash comparison', async () => {
const result = await passwordHasher.compare(plainTextPassword, '')
expect(result).toBe(false)
})
it('should return false for both empty password and hash', async () => {
const result = await passwordHasher.compare('', '')
expect(result).toBe(false)
})
})
describe('compare - Scrypt', () => {
const plainTextPassword = 'BusyBeaver$LOL99'
const salt = 'sPtDhWcd1MfdAw=='
const hashedPassword =
'iu1iqj6i6g9D7aBiE/Qdqv88GNnV/Ea67JK1kfLmzNgxsyCL8mhUxxI5VIHM9D+62xGHuZgjrfEBF+17wxyFIQ=='
it('should return true for identical scrypt passwords', async () => {
const result = await passwordHasher.compare(
plainTextPassword,
`$scrypt$${hashedPassword}${SCRYPT_SEPARATOR}${salt}`,
)
expect(result).toBe(true)
})
it('should return false for different scrypt passwords', async () => {
const differentPassword = 'differentPassword'
const result = await passwordHasher.compare(
differentPassword,
`$scrypt$${hashedPassword}${SCRYPT_SEPARATOR}${salt}`,
)
expect(result).toBe(false)
})
})
})

View File

@@ -0,0 +1,49 @@
import { PrincipalType } from '@activepieces/shared'
import { FastifyInstance } from 'fastify'
import { initializeDatabase } from '../../../../../src/app/database'
import { databaseConnection } from '../../../../../src/app/database/database-connection'
import { setupServer } from '../../../../../src/app/server'
import { generateMockToken } from '../../../../helpers/auth'
import { mockAndSaveBasicSetup } from '../../../../helpers/mocks'
let app: FastifyInstance | null = null
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('List flow runs endpoint', () => {
it('should return 200', async () => {
// arrange
const { mockPlatform, mockOwner, mockProject } = await mockAndSaveBasicSetup()
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockOwner.id,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
// act
const response = await app?.inject({
method: 'GET',
url: '/v1/flow-runs',
headers: {
authorization: `Bearer ${testToken}`,
},
query: {
projectId: mockProject.id,
},
})
// assert
expect(response?.statusCode).toBe(200)
})
})

View File

@@ -0,0 +1,69 @@
import {
apId,
PrincipalType,
} from '@activepieces/shared'
import { FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
import { generateMockToken } from '../../../helpers/auth'
import {
createMockFlow,
createMockFlowVersion,
createMockProject,
mockAndSaveBasicSetup,
} from '../../../helpers/mocks'
let app: FastifyInstance | null = null
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('Flow API for Worker', () => {
describe('Get Flow from Worker', () => {
it('List other flow for another project', async () => {
// arrange
const { mockPlatform, mockOwner, mockProject } = await mockAndSaveBasicSetup()
const mockProject2 = createMockProject({
platformId: mockPlatform.id,
ownerId: mockOwner.id,
})
await databaseConnection().getRepository('project').save([mockProject2])
const mockFlow = createMockFlow({
projectId: mockProject.id,
})
await databaseConnection().getRepository('flow').save([mockFlow])
const mockFlowVersion = createMockFlowVersion({
flowId: mockFlow.id,
})
await databaseConnection().getRepository('flow_version').save([mockFlowVersion])
const mockToken = await generateMockToken({
id: apId(),
type: PrincipalType.WORKER,
})
const response = await app?.inject({
method: 'GET',
url: `/v1/worker/flows/${mockFlowVersion.id}`,
headers: {
authorization: `Bearer ${mockToken}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.NOT_FOUND)
})
})
})

View File

@@ -0,0 +1,646 @@
import { WebhookRenewStrategy } from '@activepieces/pieces-framework'
import {
FlowOperationType,
FlowStatus,
FlowTriggerType,
FlowVersionState,
PackageType,
PieceType,
PopulatedFlow,
PrincipalType,
PropertyExecutionType,
TriggerStrategy,
TriggerTestStrategy,
WebhookHandshakeStrategy,
} from '@activepieces/shared'
import { FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
import { generateMockToken } from '../../../helpers/auth'
import {
createMockFlow,
createMockFlowVersion,
createMockPieceMetadata,
mockAndSaveBasicSetup,
} from '../../../helpers/mocks'
let app: FastifyInstance | null = null
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('Flow API', () => {
describe('Create Flow endpoint', () => {
it('Adds an empty flow', async () => {
const { mockProject, mockOwner, mockPlatform } = await mockAndSaveBasicSetup()
const mockToken = await generateMockToken({
type: PrincipalType.USER,
projectId: mockProject.id,
id: mockOwner.id,
platform: {
id: mockPlatform.id,
},
})
const mockCreateFlowRequest = {
displayName: 'test flow',
projectId: mockProject.id,
metadata: {
foo: 'bar',
},
}
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/flows',
query: {
projectId: mockProject.id,
},
headers: {
authorization: `Bearer ${mockToken}`,
},
body: mockCreateFlowRequest,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.CREATED)
const responseBody = response?.json()
expect(Object.keys(responseBody)).toHaveLength(12)
expect(responseBody?.id).toHaveLength(21)
expect(responseBody?.created).toBeDefined()
expect(responseBody?.updated).toBeDefined()
expect(responseBody?.projectId).toBe(mockProject.id)
expect(responseBody?.folderId).toBeNull()
expect(responseBody?.status).toBe('DISABLED')
expect(responseBody?.publishedVersionId).toBeNull()
expect(responseBody?.metadata).toMatchObject({ foo: 'bar' })
expect(responseBody?.operationStatus).toBeDefined()
expect(Object.keys(responseBody?.version)).toHaveLength(13)
expect(responseBody?.version?.id).toHaveLength(21)
expect(responseBody?.version?.created).toBeDefined()
expect(responseBody?.version?.updated).toBeDefined()
expect(responseBody?.version?.updatedBy).toBeNull()
expect(responseBody?.version?.flowId).toBe(responseBody?.id)
expect(responseBody?.version?.displayName).toBe('test flow')
expect(Object.keys(responseBody?.version?.trigger)).toHaveLength(5)
expect(responseBody?.version?.trigger.type).toBe('EMPTY')
expect(responseBody?.version?.trigger.name).toBe('trigger')
expect(responseBody?.version?.trigger.settings).toMatchObject({})
expect(responseBody?.version?.trigger.valid).toBe(false)
expect(responseBody?.version?.trigger.displayName).toBe('Select Trigger')
expect(responseBody?.version?.valid).toBe(false)
expect(responseBody?.version?.state).toBe('DRAFT')
})
})
describe('Update status endpoint', () => {
it('Enables a disabled Flow', async () => {
// arrange
const { mockProject, mockOwner, mockPlatform } = await mockAndSaveBasicSetup()
const mockPieceMetadata1 = createMockPieceMetadata({
name: '@activepieces/piece-schedule',
version: '0.1.5',
triggers: {
'every_hour': {
'name': 'every_hour',
'displayName': 'Every Hour',
'description': 'Triggers the current flow every hour',
'requireAuth': false,
'props': {
},
'type': TriggerStrategy.POLLING,
'sampleData': {
},
'testStrategy': TriggerTestStrategy.TEST_FUNCTION,
},
},
pieceType: PieceType.OFFICIAL,
packageType: PackageType.REGISTRY,
})
await databaseConnection()
.getRepository('piece_metadata')
.save([mockPieceMetadata1])
const mockFlow = createMockFlow({
projectId: mockProject.id,
status: FlowStatus.DISABLED,
})
await databaseConnection().getRepository('flow').save([mockFlow])
const mockFlowVersion = createMockFlowVersion({
flowId: mockFlow.id,
updatedBy: mockOwner.id,
trigger: {
type: FlowTriggerType.PIECE,
settings: {
pieceName: '@activepieces/piece-schedule',
pieceVersion: '0.1.5',
input: {
run_on_weekends: false,
},
triggerName: 'every_hour',
propertySettings: {
'run_on_weekends': {
type: PropertyExecutionType.MANUAL,
},
},
},
valid: true,
name: 'trigger',
displayName: 'Schedule',
},
})
await databaseConnection()
.getRepository('flow_version')
.save([mockFlowVersion])
await databaseConnection().getRepository('flow').update(mockFlow.id, {
publishedVersionId: mockFlowVersion.id,
})
const mockToken = await generateMockToken({
type: PrincipalType.USER,
platform: {
id: mockPlatform.id,
},
projectId: mockProject.id,
id: mockOwner.id,
})
// act
const response = await app?.inject({
method: 'POST',
url: `/v1/flows/${mockFlow.id}`,
headers: {
authorization: `Bearer ${mockToken}`,
},
body: {
type: FlowOperationType.CHANGE_STATUS,
request: {
status: 'ENABLED',
},
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
const responseBody: PopulatedFlow | undefined = response?.json()
expect(responseBody).toBeDefined()
if (responseBody) {
expect(responseBody.id).toBe(mockFlow.id)
expect(responseBody.created).toBeDefined()
expect(responseBody.updated).toBeDefined()
expect(responseBody.projectId).toBe(mockProject.id)
expect(responseBody.folderId).toBeNull()
expect(responseBody.publishedVersionId).toBe(mockFlowVersion.id)
expect(responseBody.metadata).toBeNull()
expect(responseBody.operationStatus).toBe('ENABLING')
expect(Object.keys(responseBody.version)).toHaveLength(13)
expect(responseBody.version.id).toBe(mockFlowVersion.id)
}
})
it('Disables an enabled Flow', async () => {
// arrange
const { mockProject, mockOwner, mockPlatform } = await mockAndSaveBasicSetup()
const mockFlow = createMockFlow({
projectId: mockProject.id,
status: FlowStatus.ENABLED,
})
await databaseConnection().getRepository('flow').save([mockFlow])
const mockFlowVersion = createMockFlowVersion({
flowId: mockFlow.id,
updatedBy: mockOwner.id,
})
await databaseConnection()
.getRepository('flow_version')
.save([mockFlowVersion])
await databaseConnection().getRepository('flow').update(mockFlow.id, {
publishedVersionId: mockFlowVersion.id,
})
const mockToken = await generateMockToken({
type: PrincipalType.USER,
platform: {
id: mockPlatform.id,
},
projectId: mockProject.id,
id: mockOwner.id,
})
const mockUpdateFlowStatusRequest = {
type: FlowOperationType.CHANGE_STATUS,
request: {
status: 'DISABLED',
},
}
// act
const response = await app?.inject({
method: 'POST',
url: `/v1/flows/${mockFlow.id}`,
headers: {
authorization: `Bearer ${mockToken}`,
},
body: mockUpdateFlowStatusRequest,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
const responseBody = response?.json()
expect(Object.keys(responseBody)).toHaveLength(12)
expect(responseBody?.id).toBe(mockFlow.id)
expect(responseBody?.created).toBeDefined()
expect(responseBody?.updated).toBeDefined()
expect(responseBody?.projectId).toBe(mockProject.id)
expect(responseBody?.folderId).toBeNull()
expect(responseBody?.status).toBe('ENABLED')
expect(responseBody?.publishedVersionId).toBe(mockFlowVersion.id)
expect(responseBody?.metadata).toBeNull()
expect(responseBody?.operationStatus).toBe('DISABLING')
expect(Object.keys(responseBody?.version)).toHaveLength(13)
expect(responseBody?.version?.id).toBe(mockFlowVersion.id)
})
})
describe('Update published version id endpoint', () => {
it('Publishes latest draft version', async () => {
// arrange
const { mockProject, mockOwner, mockPlatform } = await mockAndSaveBasicSetup()
const mockPieceMetadata1 = createMockPieceMetadata({
name: '@activepieces/piece-schedule',
version: '0.1.5',
triggers: {
'every_hour': {
'name': 'every_hour',
'displayName': 'Every Hour',
'description': 'Triggers the current flow every hour',
'requireAuth': true,
'props': {
},
'type': TriggerStrategy.WEBHOOK,
'handshakeConfiguration': {
'strategy': WebhookHandshakeStrategy.NONE,
},
'renewConfiguration': {
'strategy': WebhookRenewStrategy.NONE,
},
'sampleData': {
},
'testStrategy': TriggerTestStrategy.TEST_FUNCTION,
},
},
pieceType: PieceType.OFFICIAL,
packageType: PackageType.REGISTRY,
})
await databaseConnection()
.getRepository('piece_metadata')
.save([mockPieceMetadata1])
const mockFlow = createMockFlow({
projectId: mockProject.id,
status: FlowStatus.DISABLED,
})
await databaseConnection().getRepository('flow').save([mockFlow])
const mockFlowVersion = createMockFlowVersion({
flowId: mockFlow.id,
updatedBy: mockOwner.id,
state: FlowVersionState.DRAFT,
trigger: {
type: FlowTriggerType.PIECE,
settings: {
pieceName: '@activepieces/piece-schedule',
pieceVersion: '0.1.5',
input: {
run_on_weekends: false,
},
triggerName: 'every_hour',
propertySettings: {
'run_on_weekends': {
type: PropertyExecutionType.MANUAL,
},
},
},
valid: true,
name: 'trigger',
displayName: 'Schedule',
},
})
await databaseConnection()
.getRepository('flow_version')
.save([mockFlowVersion])
const mockToken = await generateMockToken({
id: mockOwner.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
// act
const response = await app?.inject({
method: 'POST',
url: `/v1/flows/${mockFlow.id}`,
body: {
type: FlowOperationType.LOCK_AND_PUBLISH,
request: {},
},
headers: {
authorization: `Bearer ${mockToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
const responseBody: PopulatedFlow | undefined = response?.json()
expect(responseBody).toBeDefined()
if (responseBody) {
expect(Object.keys(responseBody)).toHaveLength(12)
expect(responseBody.id).toBe(mockFlow.id)
expect(responseBody.created).toBeDefined()
expect(responseBody.updated).toBeDefined()
expect(responseBody.projectId).toBe(mockProject.id)
expect(responseBody.folderId).toBeNull()
expect(responseBody.status).toBe(mockFlow.status)
expect(responseBody.publishedVersionId).toBe(mockFlowVersion.id)
expect(responseBody.metadata).toBeNull()
expect(responseBody.operationStatus).toBe('DISABLING')
expect(Object.keys(responseBody.version)).toHaveLength(13)
expect(responseBody.version.id).toBe(mockFlowVersion.id)
expect(responseBody.version.state).toBe('LOCKED')
}
})
})
describe('List Flows endpoint', () => {
it('Filters Flows by status', async () => {
// arrange
const { mockProject, mockOwner, mockPlatform } = await mockAndSaveBasicSetup()
const mockEnabledFlow = createMockFlow({
projectId: mockProject.id,
status: FlowStatus.ENABLED,
})
const mockDisabledFlow = createMockFlow({
projectId: mockProject.id,
status: FlowStatus.DISABLED,
})
await databaseConnection()
.getRepository('flow')
.save([mockEnabledFlow, mockDisabledFlow])
const mockEnabledFlowVersion = createMockFlowVersion({
flowId: mockEnabledFlow.id,
})
const mockDisabledFlowVersion = createMockFlowVersion({
flowId: mockDisabledFlow.id,
})
await databaseConnection()
.getRepository('flow_version')
.save([mockEnabledFlowVersion, mockDisabledFlowVersion])
const mockToken = await generateMockToken({
type: PrincipalType.USER,
projectId: mockProject.id,
id: mockOwner.id,
platform: {
id: mockPlatform.id,
},
})
// act
const response = await app?.inject({
method: 'GET',
url: '/v1/flows',
query: {
projectId: mockProject.id,
status: 'ENABLED',
},
headers: {
authorization: `Bearer ${mockToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
const responseBody = response?.json()
expect(responseBody.data).toHaveLength(1)
expect(responseBody.data[0].id).toBe(mockEnabledFlow.id)
})
it('Populates Flow version', async () => {
// arrange
const { mockProject, mockOwner, mockPlatform } = await mockAndSaveBasicSetup()
const mockFlow = createMockFlow({ projectId: mockProject.id })
await databaseConnection().getRepository('flow').save([mockFlow])
const mockFlowVersion = createMockFlowVersion({ flowId: mockFlow.id })
await databaseConnection()
.getRepository('flow_version')
.save([mockFlowVersion])
const mockToken = await generateMockToken({
type: PrincipalType.USER,
projectId: mockProject.id,
id: mockOwner.id,
platform: {
id: mockPlatform.id,
},
})
// act
const response = await app?.inject({
method: 'GET',
url: '/v1/flows',
query: {
projectId: mockProject.id,
},
headers: {
authorization: `Bearer ${mockToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
const responseBody = response?.json()
expect(responseBody?.data).toHaveLength(1)
expect(responseBody?.data?.[0]?.id).toBe(mockFlow.id)
expect(responseBody?.data?.[0]?.version?.id).toBe(mockFlowVersion.id)
})
it('Fails if a flow with no version exists', async () => {
// arrange
const { mockProject, mockOwner, mockPlatform } = await mockAndSaveBasicSetup()
const mockFlow = createMockFlow({ projectId: mockProject.id })
await databaseConnection().getRepository('flow').save([mockFlow])
const mockToken = await generateMockToken({
type: PrincipalType.USER,
projectId: mockProject.id,
id: mockOwner.id,
platform: {
id: mockPlatform.id,
},
})
// act
const response = await app?.inject({
method: 'GET',
url: '/v1/flows',
query: {
projectId: mockProject.id,
},
headers: {
authorization: `Bearer ${mockToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.NOT_FOUND)
const responseBody = response?.json()
expect(responseBody?.code).toBe('ENTITY_NOT_FOUND')
expect(responseBody?.params?.entityType).toBe('FlowVersion')
expect(responseBody?.params?.message).toBe(`flowId=${mockFlow.id}`)
})
})
describe('Update Metadata endpoint', () => {
it('Updates flow metadata', async () => {
// arrange
const { mockProject, mockOwner, mockPlatform } = await mockAndSaveBasicSetup()
// create a flow with no metadata
const mockFlow = createMockFlow({ projectId: mockProject.id })
await databaseConnection().getRepository('flow').save([mockFlow])
const mockFlowVersion = createMockFlowVersion({ flowId: mockFlow.id })
await databaseConnection()
.getRepository('flow_version')
.save([mockFlowVersion])
const mockToken = await generateMockToken({
type: PrincipalType.USER,
projectId: mockProject.id,
id: mockOwner.id,
platform: {
id: mockPlatform.id,
},
})
const updatedMetadata = { foo: 'bar' }
// act
const response = await app?.inject({
method: 'POST',
url: `/v1/flows/${mockFlow.id}`,
headers: {
authorization: `Bearer ${mockToken}`,
},
body: {
type: FlowOperationType.UPDATE_METADATA,
request: {
metadata: updatedMetadata,
},
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
const responseBody = response?.json()
expect(responseBody.id).toBe(mockFlow.id)
expect(responseBody.metadata).toEqual(updatedMetadata)
// Verify metadata was actually persisted in the database
const updatedFlow = await databaseConnection()
.getRepository('flow')
.findOneBy({ id: mockFlow.id })
expect(updatedFlow?.metadata).toEqual(updatedMetadata)
})
})
describe('Export Flow Template endpoint', () => {
it('Exports a flow template using an API key', async () => {
// arrange
const { mockProject, mockOwner, mockPlatform } = await mockAndSaveBasicSetup()
const mockFlow = createMockFlow({
projectId: mockProject.id,
status: FlowStatus.ENABLED,
})
await databaseConnection().getRepository('flow').save([mockFlow])
const mockFlowVersion = createMockFlowVersion({
flowId: mockFlow.id,
updatedBy: mockOwner.id,
})
await databaseConnection()
.getRepository('flow_version')
.save([mockFlowVersion])
const mockApiKey = 'test_api_key'
const mockToken = await generateMockToken({
type: PrincipalType.SERVICE,
projectId: mockProject.id,
id: mockApiKey,
platform: {
id: mockPlatform.id,
},
})
// act
const response = await app?.inject({
method: 'GET',
url: `/v1/flows/${mockFlow.id}/template`,
headers: {
authorization: `Bearer ${mockToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
const responseBody = response?.json()
expect(responseBody).toHaveProperty('name')
expect(responseBody).toHaveProperty('description')
expect(responseBody).toHaveProperty('flows')
expect(responseBody.flows).toHaveLength(1)
expect(responseBody.flows[0]).toHaveProperty('trigger')
})
})
})

View File

@@ -0,0 +1,52 @@
import { apId, PrincipalType } from '@activepieces/shared'
import { FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
import { generateMockToken } from '../../../helpers/auth'
import { mockAndSaveBasicSetup } from '../../../helpers/mocks'
let app: FastifyInstance | null = null
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('Project Worker API', () => {
describe('Get worker project endpoint', () => {
it('Returns worker project', async () => {
// arrange
const { mockProject, mockPlatform } = await mockAndSaveBasicSetup()
const mockToken = await generateMockToken({
type: PrincipalType.ENGINE,
id: apId(),
platform: {
id: mockPlatform.id,
},
projectId: mockProject.id,
})
// act
const response = await app?.inject({
method: 'GET',
url: '/v1/worker/project',
headers: {
authorization: `Bearer ${mockToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
const responseBody = response?.json()
expect(responseBody?.id).toBe(mockProject.id)
})
})
})

View File

@@ -0,0 +1,77 @@
import { FlowStatus, PrincipalType } from '@activepieces/shared'
import { FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
import { generateMockToken } from '../../../helpers/auth'
import { createMockFlow, createMockFlowVersion, mockAndSaveBasicSetup } from '../../../helpers/mocks'
let app: FastifyInstance | null = null
const MOCK_FLOW_ID = '8hfKOpm3kY1yAi1ApYOa1'
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('Webhook Service', () => {
it('should return GONE if the flow is not found', async () => {
const { mockProject, mockOwner } = await mockAndSaveBasicSetup()
const { mockPlatform } = await mockAndSaveBasicSetup()
const mockToken = await generateMockToken({
type: PrincipalType.USER,
projectId: mockProject.id,
id: mockOwner.id,
platform: {
id: mockPlatform.id,
},
})
const response = await app?.inject({
method: 'GET',
url: `/v1/webhooks/${MOCK_FLOW_ID}`,
headers: {
authorization: `Bearer ${mockToken}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.GONE)
}),
it('should return NOT FOUND if the flow is disabled', async () => {
const { mockProject, mockPlatform } = await mockAndSaveBasicSetup()
const { mockOwner } = await mockAndSaveBasicSetup()
const mockFlow = createMockFlow({
projectId: mockProject.id,
status: FlowStatus.DISABLED,
})
await databaseConnection().getRepository('flow').save([mockFlow])
const mockFlowVersion = createMockFlowVersion({
flowId: mockFlow.id,
})
await databaseConnection().getRepository('flow_version').save([mockFlowVersion])
await databaseConnection().getRepository('flow').update(mockFlow.id, {
publishedVersionId: mockFlowVersion.id,
})
const mockToken = await generateMockToken({
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
id: mockOwner.id,
})
const response = await app?.inject({
method: 'GET',
url: `/v1/webhooks/${mockFlow.id}`,
headers: {
authorization: `Bearer ${mockToken}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.NOT_FOUND)
})
})

View File

@@ -0,0 +1,288 @@
import {
apId,
PlatformRole,
PrincipalType,
UserStatus,
} from '@activepieces/shared'
import { FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
import { generateMockToken } from '../../../helpers/auth'
import {
mockAndSaveBasicSetup,
mockBasicUser,
} from '../../../helpers/mocks'
let app: FastifyInstance | null = null
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('User API', () => {
describe('List users endpoint', () => {
it('Returns a list of users', async () => {
// arrange
const { mockPlatform: mockPlatformOne, mockOwner: mockOwnerOne, mockProject: mockProjectOne } = await mockAndSaveBasicSetup()
// Create Another setup
await mockAndSaveBasicSetup()
const testToken = await generateMockToken({
id: mockOwnerOne.id,
type: PrincipalType.USER,
projectId: mockProjectOne.id,
platform: {
id: mockPlatformOne.id,
},
})
// act
const response = await app?.inject({
method: 'GET',
url: '/v1/users',
query: {
platformId: mockPlatformOne.id,
},
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
const responseBody = response?.json()
expect(Object.keys(responseBody)).toHaveLength(3)
expect(responseBody.data).toHaveLength(1)
expect(responseBody.data[0].id).toBe(mockOwnerOne.id)
expect(responseBody.data[0].password).toBeUndefined()
})
it('Requires principal to be platform owner', async () => {
// arrange
const { mockPlatform } = await mockAndSaveBasicSetup()
const { mockOwner: otherMockUser, mockProject: mockProjectOne } = await mockAndSaveBasicSetup()
const testToken = await generateMockToken({
id: otherMockUser.id,
type: PrincipalType.USER,
projectId: mockProjectOne.id,
platform: {
id: mockPlatform.id,
},
})
// act
const response = await app?.inject({
method: 'GET',
url: '/v1/users',
query: {
platformId: mockPlatform.id,
},
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
const responseBody = response?.json()
expect(responseBody?.code).toBe('AUTHORIZATION')
})
})
describe('Update user endpoint', () => {
it('Updates user status to be INACTIVE', async () => {
// arrange
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
status: UserStatus.ACTIVE,
},
})
const testToken = await generateMockToken({
id: mockOwner.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
// act
const response = await app?.inject({
method: 'POST',
url: `/v1/users/${mockUser.id}`,
headers: {
authorization: `Bearer ${testToken}`,
},
body: {
status: UserStatus.INACTIVE,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
const responseJson = response?.json()
expect(responseJson.id).toBe(mockUser.id)
expect(responseJson.password).toBeUndefined()
expect(responseJson.status).toBe(UserStatus.INACTIVE)
})
it('Fails if user doesn\'t exist', async () => {
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.ADMIN,
},
})
// arrange
const nonExistentUserId = apId()
const testToken = await generateMockToken({
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
id: mockUser.id,
})
// act
const response = await app?.inject({
method: 'POST',
url: `/v1/users/${nonExistentUserId}`,
headers: {
authorization: `Bearer ${testToken}`,
},
body: {
status: UserStatus.INACTIVE,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.NOT_FOUND)
})
it('Requires principal to be platform owner', async () => {
// arrange
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const testToken = await generateMockToken({
id: mockUser.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
// act
const response = await app?.inject({
method: 'POST',
url: `/v1/users/${mockUser.id}`,
headers: {
authorization: `Bearer ${testToken}`,
},
body: {
status: UserStatus.INACTIVE,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
const responseBody = response?.json()
expect(responseBody?.code).toBe('AUTHORIZATION')
})
})
describe('Delete user endpoint', () => {
it('Removes a user', async () => {
// arrange
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const { mockUser: mockEditor } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const mockOwnerToken = await generateMockToken({
id: mockOwner.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
// act
const response = await app?.inject({
method: 'DELETE',
url: `/v1/users/${mockEditor.id}`,
headers: {
authorization: `Bearer ${mockOwnerToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.NO_CONTENT)
})
it('Fails if user is not platform owner', async () => {
// arrange
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const mockUserToken = await generateMockToken({
id: mockUser.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
// act
const response = await app?.inject({
method: 'DELETE',
url: `/v1/users/${mockUser.id}`,
headers: {
authorization: `Bearer ${mockUserToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
const responseBody = response?.json()
expect(responseBody?.code).toBe('AUTHORIZATION')
})
})
})

View File

@@ -0,0 +1,142 @@
import { PlatformRole, PrincipalType } from '@activepieces/shared'
import { faker } from '@faker-js/faker'
import { FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
import { generateMockToken } from '../../../helpers/auth'
import {
createMockApiKey,
mockAndSaveBasicSetup,
mockBasicUser,
} from '../../../helpers/mocks'
let app: FastifyInstance | null = null
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('API Key API', () => {
describe('Create API Key API', () => {
it('should create a new API Key', async () => {
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockOwner.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
const mockApiKeyName = faker.lorem.word()
const response = await app?.inject({
method: 'POST',
url: '/v1/api-keys',
body: {
displayName: mockApiKeyName,
},
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.CREATED)
expect(responseBody.id).toHaveLength(21)
expect(responseBody.platformId).toBe(mockPlatform.id)
expect(responseBody.hashedValue).toBeUndefined()
expect(responseBody.displayName).toBe(mockApiKeyName)
expect(responseBody.truncatedValue).toHaveLength(4)
expect(responseBody.value).toHaveLength(64)
expect(responseBody.value).toContain('sk-')
})
})
describe('Delete API Key endpoint', () => {
it('Fail if non owner', async () => {
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const mockApiKey = createMockApiKey({
platformId: mockPlatform.id,
})
await databaseConnection().getRepository('api_key').save(mockApiKey)
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockUser.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
const response = await app?.inject({
method: 'DELETE',
url: `/v1/api-keys/${mockApiKey.id}`,
headers: {
authorization: `Bearer ${testToken}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
})
describe('List API Keys endpoint', () => {
it('Filters Signing Keys by platform', async () => {
// arrange
const { mockOwner: mockUserOne, mockPlatform: mockPlatformOne, mockProject: mockProjectOne } = await mockAndSaveBasicSetup()
const { mockPlatform: mockPlatformTwo } = await mockAndSaveBasicSetup()
const mockKeyOne = createMockApiKey({
platformId: mockPlatformOne.id,
})
const mockKeyTwo = createMockApiKey({
platformId: mockPlatformTwo.id,
})
await databaseConnection()
.getRepository('api_key')
.save([mockKeyOne, mockKeyTwo])
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockUserOne.id,
projectId: mockProjectOne.id,
platform: { id: mockPlatformOne.id },
})
// act
const response = await app?.inject({
method: 'GET',
url: '/v1/api-keys',
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody.data).toHaveLength(1)
expect(responseBody.data[0].id).toBe(mockKeyOne.id)
expect(responseBody.data[0].hashedValue).toBeUndefined()
})
})
})

View File

@@ -0,0 +1,309 @@
import {
AppConnectionType,
DefaultProjectRole,
PackageType,
PlatformRole,
PrincipalType,
ProjectRole,
UpsertAppConnectionRequestBody,
} from '@activepieces/shared'
import { FastifyBaseLogger, FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { pieceMetadataService } from '../../../../src/app/pieces/metadata/piece-metadata-service'
import { setupServer } from '../../../../src/app/server'
import { generateMockToken } from '../../../helpers/auth'
import {
createMockPieceMetadata,
createMockProjectMember,
mockAndSaveBasicSetup,
mockBasicUser,
} from '../../../helpers/mocks'
let app: FastifyInstance | null = null
let mockLog: FastifyBaseLogger
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
mockLog = app!.log!
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('AppConnection API', () => {
describe('Upsert AppConnection endpoint', () => {
it('Succeeds with metadata field', async () => {
// arrange
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.ADMIN,
},
})
const mockPieceMetadata = createMockPieceMetadata({
projectId: mockProject.id,
platformId: mockPlatform.id,
packageType: PackageType.REGISTRY,
})
await databaseConnection().getRepository('piece_metadata').save([mockPieceMetadata])
pieceMetadataService(mockLog).getOrThrow = jest.fn().mockResolvedValue(mockPieceMetadata)
const mockToken = await generateMockToken({
id: mockUser.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
const mockUpsertAppConnectionRequest: UpsertAppConnectionRequestBody = {
externalId: 'test-app-connection-with-metadata',
displayName: 'Test Connection with Metadata',
pieceName: mockPieceMetadata.name,
projectId: mockProject.id,
type: AppConnectionType.SECRET_TEXT,
value: {
type: AppConnectionType.SECRET_TEXT,
secret_text: 'test-secret-text',
},
metadata: {
foo: 'bar',
},
pieceVersion: mockPieceMetadata.version,
}
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/app-connections',
headers: {
authorization: `Bearer ${mockToken}`,
},
body: mockUpsertAppConnectionRequest,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.CREATED)
const responseBody = response?.json()
expect(responseBody.metadata).toEqual(mockUpsertAppConnectionRequest.metadata)
expect(responseBody.pieceVersion).toEqual(mockPieceMetadata.version)
// Verify connection can be updated with new metadata
const updateResponse = await app?.inject({
method: 'POST',
url: `/v1/app-connections/${responseBody.id}`,
headers: {
authorization: `Bearer ${mockToken}`,
},
body: {
displayName: 'Updated Connection Name',
metadata: {
foo: 'baz',
},
},
})
expect(updateResponse?.statusCode).toBe(StatusCodes.OK)
const updatedResponseBody = updateResponse?.json()
expect(updatedResponseBody.metadata).toEqual({
foo: 'baz',
})
})
it.each([
DefaultProjectRole.ADMIN,
DefaultProjectRole.EDITOR,
])('Succeeds if user role is %s', async (testRole) => {
// arrange
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: testRole }) as ProjectRole
const mockProjectMember = createMockProjectMember({
userId: mockUser.id,
platformId: mockPlatform.id,
projectId: mockProject.id,
projectRoleId: projectRole.id,
})
await databaseConnection().getRepository('project_member').save([mockProjectMember])
const mockPieceMetadata = createMockPieceMetadata({
projectId: mockProject.id,
platformId: mockPlatform.id,
packageType: PackageType.REGISTRY,
})
await databaseConnection().getRepository('piece_metadata').save([mockPieceMetadata])
pieceMetadataService(mockLog).getOrThrow = jest.fn().mockResolvedValue(mockPieceMetadata)
const mockToken = await generateMockToken({
id: mockUser.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
const mockUpsertAppConnectionRequest: UpsertAppConnectionRequestBody = {
externalId: 'test-app-connection',
displayName: 'test-app-connection',
pieceName: mockPieceMetadata.name,
projectId: mockProject.id,
type: AppConnectionType.SECRET_TEXT,
value: {
type: AppConnectionType.SECRET_TEXT,
secret_text: 'test-secret-text',
},
pieceVersion: mockPieceMetadata.version,
}
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/app-connections',
headers: {
authorization: `Bearer ${mockToken}`,
},
body: mockUpsertAppConnectionRequest,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.CREATED)
})
it('Fails if user role is VIEWER', async () => {
// arrange
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.VIEWER }) as ProjectRole
const mockProjectMember = createMockProjectMember({
userId: mockUser.id,
platformId: mockPlatform.id,
projectId: mockProject.id,
projectRoleId: projectRole.id,
})
await databaseConnection().getRepository('project_member').save([mockProjectMember])
const mockPieceMetadata = createMockPieceMetadata({
projectId: mockProject.id,
platformId: mockPlatform.id,
})
await databaseConnection().getRepository('piece_metadata').save([mockPieceMetadata])
pieceMetadataService(mockLog).getOrThrow = jest.fn().mockResolvedValue(mockPieceMetadata)
const mockToken = await generateMockToken({
id: mockUser.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
const mockUpsertAppConnectionRequest: UpsertAppConnectionRequestBody = {
externalId: 'test-app-connection',
displayName: 'test-app-connection',
pieceName: mockPieceMetadata.name,
projectId: mockProject.id,
type: AppConnectionType.SECRET_TEXT,
value: {
type: AppConnectionType.SECRET_TEXT,
secret_text: 'test-secret-text',
},
pieceVersion: mockPieceMetadata.version,
}
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/app-connections',
headers: {
authorization: `Bearer ${mockToken}`,
},
body: mockUpsertAppConnectionRequest,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
const responseBody = response?.json()
expect(responseBody?.code).toBe('PERMISSION_DENIED')
expect(responseBody?.params?.userId).toBe(mockUser.id)
expect(responseBody?.params?.projectId).toBe(mockProject.id)
})
})
describe('List AppConnections endpoint', () => {
it.each([
DefaultProjectRole.ADMIN,
DefaultProjectRole.EDITOR,
DefaultProjectRole.VIEWER,
])('Succeeds if user role is %s', async (testRole) => {
// arrange
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: testRole }) as ProjectRole
const mockProjectMember = createMockProjectMember({
userId: mockUser.id,
platformId: mockPlatform.id,
projectId: mockProject.id,
projectRoleId: projectRole.id,
})
await databaseConnection().getRepository('project_member').save([mockProjectMember])
const mockToken = await generateMockToken({
id: mockUser.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
// act
const response = await app?.inject({
method: 'GET',
url: '/v1/app-connections',
headers: {
authorization: `Bearer ${mockToken}`,
},
query: {
projectId: mockProject.id,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
})
})
})

View File

@@ -0,0 +1,53 @@
import { FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
let app: FastifyInstance | null = null
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('AppSumo API', () => {
describe('Action endpoint', () => {
it('Activates new accounts', async () => {
// arrange
const mockEmail = 'mock-email'
const requestBody = {
action: 'activate',
plan_id: 'plan_id',
uuid: 'uuid',
activation_email: mockEmail,
}
const appSumoToken = 'app-sumo-token'
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/appsumo/action',
headers: {
authorization: `Bearer ${appSumoToken}`,
},
body: requestBody,
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.CREATED)
expect(responseBody?.message).toBe('success')
expect(responseBody?.redirect_url).toBe(
`https://cloud.activepieces.com/sign-up?email=${mockEmail}`,
)
})
})
})

View File

@@ -0,0 +1,127 @@
import { PlatformRole, PrincipalType } from '@activepieces/shared'
import { FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
import { generateMockToken } from '../../../helpers/auth'
import {
createAuditEvent,
mockAndSaveBasicSetup,
mockBasicUser,
} from '../../../helpers/mocks'
let app: FastifyInstance | null = null
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('Audit Event API', () => {
describe('List Audit event API', () => {
it('should list audit events', async () => {
// arrange
const { mockOwner: mockUserOne, mockPlatform: mockPlatformOne } = await mockAndSaveBasicSetup({
plan: {
auditLogEnabled: true,
},
})
const { mockOwner: mockUserTwo, mockPlatform: mockPlatformTwo, mockProject: mockProjectOne } = await mockAndSaveBasicSetup({
})
const testToken1 = await generateMockToken({
type: PrincipalType.USER,
id: mockUserOne.id,
projectId: mockProjectOne.id,
platform: { id: mockPlatformOne.id },
})
const mockAuditEvents1 = [
createAuditEvent({
platformId: mockPlatformOne.id,
userId: mockUserOne.id,
}),
createAuditEvent({
platformId: mockPlatformOne.id,
userId: mockUserOne.id,
}),
]
await databaseConnection()
.getRepository('audit_event')
.save(mockAuditEvents1)
const mockAuditEvents2 = [
createAuditEvent({
platformId: mockPlatformTwo.id,
userId: mockUserTwo.id,
}),
createAuditEvent({
platformId: mockPlatformTwo.id,
userId: mockUserTwo.id,
}),
]
await databaseConnection()
.getRepository('audit_event')
.save(mockAuditEvents2)
// act
const response1 = await app?.inject({
method: 'GET',
url: '/v1/audit-events',
headers: {
authorization: `Bearer ${testToken1}`,
},
})
// assert
expect(response1?.statusCode).toBe(StatusCodes.OK)
const responseBody1 = response1?.json()
expect(responseBody1.data).toHaveLength(mockAuditEvents1.length)
expect(responseBody1?.data).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: mockAuditEvents1[0].id }),
expect.objectContaining({ id: mockAuditEvents1[1].id }),
]),
)
})
it('should return forbidden if the user is not the owner', async () => {
// arrange
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockUser.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
// act
const response = await app?.inject({
method: 'GET',
url: '/v1/audit-events',
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
})
})

View File

@@ -0,0 +1,765 @@
import {
CustomDomain,
OtpType,
} from '@activepieces/ee-shared'
import {
DefaultProjectRole,
InvitationStatus,
InvitationType,
Platform,
PlatformPlan,
PlatformRole,
Project,
ProjectRole,
ProjectType,
User,
UserStatus,
} from '@activepieces/shared'
import { faker } from '@faker-js/faker'
import dayjs from 'dayjs'
import { FastifyBaseLogger, FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import * as emailServiceFile from '../../../../src/app/ee/helper/email/email-service'
import { setupServer } from '../../../../src/app/server'
import { decodeToken } from '../../../helpers/auth'
import {
CLOUD_PLATFORM_ID,
createMockCustomDomain,
createMockPlatform,
createMockPlatformPlan,
createMockProject,
createMockUserInvitation,
mockAndSaveBasicSetup,
mockBasicUser,
} from '../../../helpers/mocks'
import {
createMockSignInRequest,
createMockSignUpRequest,
} from '../../../helpers/mocks/authn'
let app: FastifyInstance | null = null
let sendOtpSpy: jest.Mock
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
beforeEach(async () => {
sendOtpSpy = jest.fn()
jest.spyOn(emailServiceFile, 'emailService').mockImplementation((_log: FastifyBaseLogger) => ({
sendOtp: sendOtpSpy,
sendInvitation: jest.fn(),
sendIssueCreatedNotification: jest.fn(),
sendQuotaAlert: jest.fn(),
sendTrialReminder: jest.fn(),
sendReminderJobHandler: jest.fn(),
sendExceedFailureThresholdAlert: jest.fn(),
}))
await databaseConnection().getRepository('flag').createQueryBuilder().delete().execute()
await databaseConnection().getRepository('project').createQueryBuilder().delete().execute()
await databaseConnection().getRepository('platform').createQueryBuilder().delete().execute()
await databaseConnection().getRepository('user').createQueryBuilder().delete().execute()
await databaseConnection().getRepository('user_identity').createQueryBuilder().delete().execute()
await databaseConnection().getRepository('custom_domain').createQueryBuilder().delete().execute()
await databaseConnection().getRepository('user_invitation').createQueryBuilder().delete().execute()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('Authentication API', () => {
describe('Sign up Endpoint', () => {
it('Add new user if the domain is allowed', async () => {
// arrange
const { mockPlatform, mockUser, mockCustomDomain } =
await createMockPlatformAndDomain({
platform: {
id: CLOUD_PLATFORM_ID,
emailAuthEnabled: true,
},
plan: {
ssoEnabled: false,
},
})
const mockSignUpRequest = createMockSignUpRequest()
await databaseConnection()
.getRepository('platform')
.update(mockPlatform.id, {
enforceAllowedAuthDomains: true,
allowedAuthDomains: [mockSignUpRequest.email.split('@')[1]],
})
const mockProject = createMockProject({
ownerId: mockUser.id,
platformId: mockPlatform.id,
})
await databaseConnection().getRepository('project').save(mockProject)
const mockUserInvitation = createMockUserInvitation({
platformId: mockPlatform.id,
email: mockSignUpRequest.email,
platformRole: PlatformRole.ADMIN,
type: InvitationType.PLATFORM,
status: InvitationStatus.ACCEPTED,
created: dayjs().toISOString(),
})
await databaseConnection()
.getRepository('user_invitation')
.save(mockUserInvitation)
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/authentication/sign-up',
body: mockSignUpRequest,
headers: {
Host: mockCustomDomain.domain,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
})
it('Fails If the domain is not allowed', async () => {
// arrange
const { mockCustomDomain } =
await createMockPlatformAndDomain({
platform: {
emailAuthEnabled: true,
enforceAllowedAuthDomains: true,
allowedAuthDomains: [],
},
plan: {
ssoEnabled: true,
},
})
const mockSignUpRequest = createMockSignUpRequest()
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/authentication/sign-up',
body: mockSignUpRequest,
headers: {
Host: mockCustomDomain.domain,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
const responseBody = response?.json()
expect(responseBody?.code).toBe('DOMAIN_NOT_ALLOWED')
})
it('Create new user for the cloud user and then ask to verify email if email is not verified', async () => {
await createMockPlatformAndDomain({
platform: {
id: CLOUD_PLATFORM_ID,
emailAuthEnabled: true,
},
plan: {
ssoEnabled: false,
},
})
// arrange
const mockSignUpRequest = createMockSignUpRequest()
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/authentication/sign-up',
body: mockSignUpRequest,
headers: {
},
})
const responseBody = response?.json()
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
expect(responseBody).toEqual({
code: 'EMAIL_IS_NOT_VERIFIED',
params: {
email: mockSignUpRequest.email.toLocaleLowerCase().trim(),
},
})
})
it('Sends a verification email', async () => {
// arrange
const mockSignUpRequest = createMockSignUpRequest()
await createMockPlatformAndDomain({
platform: {
id: CLOUD_PLATFORM_ID,
emailAuthEnabled: true,
enforceAllowedAuthDomains: false,
},
plan: {
ssoEnabled: false,
},
})
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/authentication/sign-up',
body: mockSignUpRequest,
headers: {
},
})
const responseBody = response?.json()
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
expect(responseBody).toEqual({
code: 'EMAIL_IS_NOT_VERIFIED',
params: {
email: mockSignUpRequest.email.toLocaleLowerCase().trim(),
},
})
expect(sendOtpSpy).toHaveBeenCalledTimes(1)
expect(sendOtpSpy).toHaveBeenCalledWith({
otp: expect.stringMatching(/^([0-9A-F]|-){36}$/i),
platformId: expect.any(String),
type: OtpType.EMAIL_VERIFICATION,
userIdentity: expect.objectContaining({
email: mockSignUpRequest.email.trim().toLocaleLowerCase(),
}),
})
})
it('auto verify invited users to continue platform sign up', async () => {
const {
mockUser: mockPlatformOwner,
mockPlatform,
mockCustomDomain,
} = await createMockPlatformAndDomain({
platform: {
},
plan: {
ssoEnabled: false,
projectRolesEnabled: true,
},
})
const mockProject = createMockProject({
ownerId: mockPlatformOwner.id,
platformId: mockPlatform.id,
})
await databaseConnection().getRepository('project').save(mockProject)
const editorRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.EDITOR }) as ProjectRole
const mockedUpEmail = faker.internet.email()
const mockUserInvitation = createMockUserInvitation({
projectId: mockProject.id,
platformId: mockPlatform.id,
email: mockedUpEmail,
projectRole: editorRole,
type: InvitationType.PROJECT,
status: InvitationStatus.ACCEPTED,
created: dayjs().toISOString(),
})
await databaseConnection()
.getRepository('user_invitation')
.save(mockUserInvitation)
const mockSignUpRequest = createMockSignUpRequest({
email: mockedUpEmail,
})
const response = await app?.inject({
method: 'POST',
url: '/v1/authentication/sign-up',
headers: {
Host: mockCustomDomain.domain,
},
body: mockSignUpRequest,
})
const responseBody = response?.json()
const projects = await databaseConnection().getRepository('project').find({ where: { ownerId: responseBody?.id } })
expect(projects.length).toBe(1)
expect(projects[0].type).toBe(ProjectType.PERSONAL)
const teamProject = await databaseConnection().getRepository('project').findOne({ where: { displayName: mockProject.displayName } })
expect(teamProject).toBeDefined()
const projectMember = await databaseConnection().getRepository('project_member').findOne({ where: { projectId: teamProject?.id, userId: responseBody?.id } })
expect(projectMember).toBeDefined()
expect(projectMember?.userId).toBe(responseBody?.id)
expect(projectMember?.projectId).toBe(teamProject?.id)
expect(projectMember?.platformId).toBe(mockPlatform.id)
expect(projectMember?.projectRoleId).toBe(editorRole.id)
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody?.platformId).toBeDefined()
expect(responseBody?.status).toBe('ACTIVE')
expect(responseBody?.verified).toBe(true)
})
it('fails to sign up invited user platform if no project exist', async () => {
// arrange
const { mockCustomDomain } = await createMockPlatformAndDomain({
platform: {
emailAuthEnabled: true,
enforceAllowedAuthDomains: false,
},
plan: {
ssoEnabled: false,
},
})
const mockedUpEmail = faker.internet.email()
const mockSignUpRequest = createMockSignUpRequest({
email: mockedUpEmail,
})
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/authentication/sign-up',
headers: {
Host: mockCustomDomain.domain,
},
body: mockSignUpRequest,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
const responseBody = response?.json()
expect(responseBody?.code).toBe('INVITATION_ONLY_SIGN_UP')
})
})
describe('Sign in Endpoint', () => {
it('Fails If the email auth is not enabled', async () => {
// arrange
const rawPassword = faker.internet.password()
const { mockPlatform, mockCustomDomain } = await createMockPlatformAndDomain({
platform: {
emailAuthEnabled: false,
},
plan: {
ssoEnabled: true,
},
})
const { mockUserIdentity } = await mockBasicUser({
user: {
status: UserStatus.ACTIVE,
platformId: mockPlatform.id,
platformRole: PlatformRole.ADMIN,
},
userIdentity: {
email: faker.internet.email(),
password: rawPassword,
verified: true,
},
})
const mockSignInRequest = createMockSignInRequest({
email: mockUserIdentity.email,
password: rawPassword,
})
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/authentication/sign-in',
headers: {
Host: mockCustomDomain.domain,
},
body: mockSignInRequest,
})
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
const responseBody = response?.json()
expect(responseBody?.code).toBe('EMAIL_AUTH_DISABLED')
})
it('Fails If the domain is not allowed', async () => {
// arrange
const mockPlatformId = faker.string.nanoid()
const mockPlatformDomain = faker.internet.domainName()
await createMockPlatformAndDomain({
platform: {
id: mockPlatformId,
allowedAuthDomains: [mockPlatformDomain],
enforceAllowedAuthDomains: true,
emailAuthEnabled: true,
},
plan: {
ssoEnabled: true,
},
domain: {
domain: mockPlatformDomain,
},
})
const rawPassword = faker.internet.password()
const { mockUserIdentity } = await mockBasicUser({
user: {
status: UserStatus.ACTIVE,
platformId: mockPlatformId,
platformRole: PlatformRole.ADMIN,
},
userIdentity: {
email: faker.internet.email(),
password: rawPassword,
verified: true,
},
})
const mockSignInRequest = createMockSignInRequest({
email: mockUserIdentity.email,
password: rawPassword,
})
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/authentication/sign-in',
headers: {
Host: mockPlatformDomain,
},
body: mockSignInRequest,
})
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
const responseBody = response?.json()
expect(responseBody?.code).toBe('DOMAIN_NOT_ALLOWED')
})
it('Logs in existing users', async () => {
// arrange
const mockEmail = faker.internet.email()
const mockPassword = 'password'
const { mockPlatform, mockProject } = await createMockPlatformAndDomain({
platform: {
emailAuthEnabled: true,
enforceAllowedAuthDomains: false,
},
plan: {
embeddingEnabled: false,
ssoEnabled: false,
},
})
const { mockUser, mockUserIdentity } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
status: UserStatus.ACTIVE,
platformRole: PlatformRole.ADMIN,
},
userIdentity: {
email: mockEmail,
password: mockPassword,
verified: true,
},
})
const mockSignInRequest = createMockSignInRequest({
email: mockEmail,
password: mockPassword,
})
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/authentication/sign-in',
body: mockSignInRequest,
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody?.id).toBe(mockUser.id)
expect(responseBody?.email.toLocaleLowerCase().trim()).toBe(mockEmail.toLocaleLowerCase().trim())
expect(responseBody?.firstName).toBe(mockUserIdentity.firstName)
expect(responseBody?.lastName).toBe(mockUserIdentity.lastName)
expect(responseBody?.trackEvents).toBe(mockUserIdentity.trackEvents)
expect(responseBody?.newsLetter).toBe(mockUserIdentity.newsLetter)
expect(responseBody?.password).toBeUndefined()
expect(responseBody?.status).toBe(mockUser.status)
expect(responseBody?.verified).toBe(mockUserIdentity.verified)
expect(responseBody?.platformId).toBe(mockPlatform.id)
expect(responseBody?.externalId).toBe(null)
expect(responseBody?.projectId).toBe(mockProject.id)
expect(responseBody?.token).toBeDefined()
})
it('Signs in platform users', async () => {
// arrange
const mockEmail = faker.internet.email()
const mockPassword = 'password'
const mockPlatformId = faker.string.nanoid()
const mockPlatformDomain = faker.internet.domainName()
await createMockPlatformAndDomain({
platform: {
id: mockPlatformId,
emailAuthEnabled: true,
enforceAllowedAuthDomains: false,
},
plan: {
ssoEnabled: false,
},
domain: {
domain: mockPlatformDomain,
platformId: mockPlatformId,
},
})
const { mockUser } = await mockBasicUser({
user: {
status: UserStatus.ACTIVE,
platformId: mockPlatformId,
platformRole: PlatformRole.ADMIN,
},
userIdentity: {
email: mockEmail,
password: mockPassword,
verified: true,
},
})
await databaseConnection().getRepository('user').save(mockUser)
const mockProject = createMockProject({
ownerId: mockUser.id,
platformId: mockPlatformId,
})
await databaseConnection().getRepository('project').save(mockProject)
const mockSignInRequest = createMockSignInRequest({
email: mockEmail,
password: mockPassword,
})
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/authentication/sign-in',
headers: {
Host: mockPlatformDomain,
},
body: mockSignInRequest,
})
const responseBody = response?.json()
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody?.platformId).toBe(mockPlatformId)
const decodedToken = decodeToken(responseBody?.token)
expect(decodedToken?.platform?.id).toBe(mockPlatformId)
})
it('Fails to sign in platform users if no project exists', async () => {
// arrange
const { mockPlatform, mockCustomDomain } = await createMockPlatformAndDomain({
platform: {
emailAuthEnabled: true,
enforceAllowedAuthDomains: false,
},
plan: {
ssoEnabled: true,
},
})
const mockPassword = 'password'
const mockUserIdentityEmail = faker.internet.email()
await mockBasicUser({
user: {
status: UserStatus.ACTIVE,
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
userIdentity: {
email: mockUserIdentityEmail,
password: mockPassword,
verified: true,
},
})
const mockSignInRequest = createMockSignInRequest({
email: mockUserIdentityEmail,
password: mockPassword,
})
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/authentication/sign-in',
headers: {
Host: mockCustomDomain.domain,
},
body: mockSignInRequest,
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
expect(responseBody?.code).toBe('INVITATION_ONLY_SIGN_UP')
})
it('Fails if password doesn\'t match', async () => {
// arrange
const mockEmail = faker.internet.email()
const mockPassword = 'password'
const { mockUser } = await mockBasicUser({
user: {
status: UserStatus.ACTIVE,
},
userIdentity: {
email: mockEmail,
password: mockPassword,
verified: true,
},
})
const mockPlatform = createMockPlatform({
id: CLOUD_PLATFORM_ID,
ownerId: mockUser.id,
})
await databaseConnection().getRepository('platform').save(mockPlatform)
const mockProject = createMockProject({
ownerId: mockUser.id,
platformId: mockPlatform.id,
})
await databaseConnection().getRepository('project').save(mockProject)
const mockSignInRequest = createMockSignInRequest({
email: mockEmail,
password: 'wrong password',
})
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/authentication/sign-in',
body: mockSignInRequest,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.UNAUTHORIZED)
const responseBody = response?.json()
expect(responseBody?.code).toBe('INVALID_CREDENTIALS')
})
it('Fails if user status is INACTIVE', async () => {
// arrange
const mockEmail = faker.internet.email()
const mockPassword = 'password'
const { mockUser } = await mockBasicUser({
user: {
status: UserStatus.INACTIVE,
platformRole: PlatformRole.ADMIN,
},
userIdentity: {
email: mockEmail,
password: mockPassword,
verified: true,
},
})
const mockPlatform = createMockPlatform({
ownerId: mockUser.id,
emailAuthEnabled: true,
enforceAllowedAuthDomains: false,
})
await databaseConnection().getRepository('platform').save(mockPlatform)
const mockPlatformPlan = createMockPlatformPlan({
platformId: mockPlatform.id,
ssoEnabled: false,
})
await databaseConnection().getRepository('platform_plan').save(mockPlatformPlan)
await databaseConnection().getRepository('user').update(mockUser.id, {
platformId: mockPlatform.id,
})
const mockProject = createMockProject({
ownerId: mockUser.id,
platformId: mockPlatform.id,
})
await databaseConnection().getRepository('project').save(mockProject)
const mockSignInRequest = createMockSignInRequest({
email: mockEmail,
password: mockPassword,
})
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/authentication/sign-in',
body: mockSignInRequest,
})
const responseBody = response?.json()
// assert
expect(response?.statusCode).toBe(StatusCodes.UNAUTHORIZED)
expect(responseBody?.code).toBe('AUTHENTICATION')
expect(responseBody?.params.message).toBe('No platform found for identity')
})
})
})
async function createMockPlatformAndDomain({ platform, domain, plan }: { platform: Partial<Platform>, domain?: Partial<CustomDomain>, plan?: Partial<PlatformPlan> }): Promise<{
mockUser: User
mockPlatform: Platform
mockCustomDomain: CustomDomain
mockProject: Project
}> {
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup({
platform,
})
const mockCustomDomain = createMockCustomDomain({
platformId: mockPlatform.id,
...domain,
})
await databaseConnection()
.getRepository('custom_domain')
.save(mockCustomDomain)
const mockPlatformPlan = createMockPlatformPlan({
platformId: mockPlatform.id,
...plan,
})
await databaseConnection().getRepository('platform_plan').upsert(mockPlatformPlan, ['platformId'])
return { mockUser: mockOwner, mockPlatform, mockCustomDomain, mockProject }
}

View File

@@ -0,0 +1,265 @@
import { OtpState, OtpType } from '@activepieces/ee-shared'
import { UserStatus } from '@activepieces/shared'
import dayjs from 'dayjs'
import { FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
import { createMockOtp, mockBasicUser } from '../../../helpers/mocks'
let app: FastifyInstance | null = null
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('Enterprise Local Authn API', () => {
describe('Verify Email Endpoint', () => {
it('Verifies user', async () => {
const { mockUserIdentity } = await mockBasicUser({
user: {
status: UserStatus.ACTIVE,
},
userIdentity: {
verified: false,
},
})
const mockOtp = createMockOtp({
identityId: mockUserIdentity.id,
type: OtpType.EMAIL_VERIFICATION,
state: OtpState.PENDING,
})
await databaseConnection().getRepository('otp').save(mockOtp)
const mockVerifyEmailRequest = {
identityId: mockUserIdentity.id,
otp: mockOtp.value,
}
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/authn/local/verify-email',
body: mockVerifyEmailRequest,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(response?.body).toBe('')
const userIdentity = await databaseConnection()
.getRepository('user_identity')
.findOneBy({ id: mockUserIdentity.id })
expect(userIdentity?.verified).toBe(true)
const otp = await databaseConnection()
.getRepository('otp')
.findOneBy({ id: mockOtp.id })
expect(otp?.state).toBe(OtpState.CONFIRMED)
})
it('Fails if OTP is wrong', async () => {
const { mockUserIdentity } = await mockBasicUser({
user: {
status: UserStatus.ACTIVE,
},
userIdentity: {
verified: false,
},
})
const correctOtp = '123456'
const mockOtp = createMockOtp({
identityId: mockUserIdentity.id,
type: OtpType.EMAIL_VERIFICATION,
value: correctOtp,
state: OtpState.PENDING,
})
await databaseConnection().getRepository('otp').save(mockOtp)
const incorrectOtp = '654321'
const mockVerifyEmailRequest = {
identityId: mockUserIdentity.id,
otp: incorrectOtp,
}
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/authn/local/verify-email',
body: mockVerifyEmailRequest,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.GONE)
const responseBody = response?.json()
expect(responseBody?.code).toBe('INVALID_OTP')
const userIdentity = await databaseConnection()
.getRepository('user_identity')
.findOneBy({ id: mockUserIdentity.id })
expect(userIdentity?.verified).toBe(false)
})
it('Fails if OTP has expired', async () => {
const { mockUserIdentity } = await mockBasicUser({
user: {
status: UserStatus.ACTIVE,
},
userIdentity: {
verified: false,
},
})
const mockOtp = createMockOtp({
identityId: mockUserIdentity.id,
type: OtpType.EMAIL_VERIFICATION,
updated: dayjs().subtract(31, 'minutes').toISOString(),
state: OtpState.PENDING,
})
await databaseConnection().getRepository('otp').save(mockOtp)
const mockVerifyEmailRequest = {
identityId: mockUserIdentity.id,
otp: mockOtp.value,
}
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/authn/local/verify-email',
body: mockVerifyEmailRequest,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.GONE)
const responseBody = response?.json()
expect(responseBody?.code).toBe('INVALID_OTP')
const userIdentity = await databaseConnection()
.getRepository('user_identity')
.findOneBy({ id: mockUserIdentity.id })
expect(userIdentity?.verified).toBe(false)
})
it('Fails if OTP was confirmed before', async () => {
const { mockUserIdentity } = await mockBasicUser({
user: {
status: UserStatus.ACTIVE,
},
userIdentity: {
verified: false,
},
})
const mockOtp = createMockOtp({
identityId: mockUserIdentity.id,
type: OtpType.EMAIL_VERIFICATION,
state: OtpState.CONFIRMED,
})
await databaseConnection().getRepository('otp').save(mockOtp)
const mockVerifyEmailRequest = {
identityId: mockUserIdentity.id,
otp: mockOtp.value,
}
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/authn/local/verify-email',
body: mockVerifyEmailRequest,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.GONE)
const responseBody = response?.json()
expect(responseBody?.code).toBe('INVALID_OTP')
const userIdentity = await databaseConnection()
.getRepository('user_identity')
.findOneBy({ id: mockUserIdentity.id })
expect(userIdentity?.verified).toBe(false)
})
})
describe('Reset Password Endpoint', () => {
it('Updates user password', async () => {
const { mockUserIdentity } = await mockBasicUser({
userIdentity: { },
})
const mockOtp = createMockOtp({
identityId: mockUserIdentity.id,
type: OtpType.PASSWORD_RESET,
state: OtpState.PENDING,
})
await databaseConnection().getRepository('otp').save(mockOtp)
const mockResetPasswordRequest = {
identityId: mockUserIdentity.id,
otp: mockOtp.value,
newPassword: 'newPassword',
}
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/authn/local/reset-password',
body: mockResetPasswordRequest,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(response?.body).toBe('')
const userIdentity = await databaseConnection()
.getRepository('user_identity')
.findOneBy({ id: mockUserIdentity.id })
expect(userIdentity?.password).not.toBe(mockUserIdentity.password)
})
it('Fails if OTP is wrong', async () => {
const { mockUserIdentity } = await mockBasicUser({
})
const correctOtp = '123456'
const mockOtp = createMockOtp({
identityId: mockUserIdentity.id,
type: OtpType.PASSWORD_RESET,
value: correctOtp,
})
await databaseConnection().getRepository('otp').save(mockOtp)
const incorrectOtp = '654321'
const mockResetPasswordRequest = {
identityId: mockUserIdentity.id,
otp: incorrectOtp,
newPassword: 'newPassword',
}
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/authn/local/reset-password',
body: mockResetPasswordRequest,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.GONE)
const responseBody = response?.json()
expect(responseBody?.code).toBe('INVALID_OTP')
const userIdentity = await databaseConnection()
.getRepository('user_identity')
.findOneBy({ id: mockUserIdentity.id })
expect(userIdentity?.password).toBe(mockUserIdentity.password)
})
})
})

View File

@@ -0,0 +1,685 @@
import {
ActivepiecesError,
ALL_PRINCIPAL_TYPES,
apId,
EndpointScope,
ErrorCode,
Principal,
PrincipalType,
} from '@activepieces/shared'
import { FastifyInstance, FastifyRequest } from 'fastify'
import { nanoid } from 'nanoid'
import { securityHandlerChain } from '../../../../src/app/core/security/security-handler-chain'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
import { generateMockToken } from '../../../helpers/auth'
import {
createMockFlow,
mockAndSaveBasicSetup,
mockAndSaveBasicSetupWithApiKey,
} from '../../../helpers/mocks'
let app: FastifyInstance | null = null
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('API Security', () => {
describe('Webhook Authentication', () => {
it('Skips principal authentication and authorization for webhook routes', async () => {
// arrange
const routes = [
'/v1/webhooks',
'/v1/webhooks/:flowId',
'/v1/webhooks/:flowId/simulate',
'/v1/webhooks/:flowId/sync',
]
for (const route of routes) {
const mockRequest = {
method: 'POST',
routeOptions: {
url: route,
config: {
skipAuth: true,
allowedPrincipals: ALL_PRINCIPAL_TYPES,
},
},
headers: {
},
} as unknown as FastifyRequest
// act
const result = securityHandlerChain(mockRequest)
// assert
await expect(result).resolves.toBeUndefined()
expect(mockRequest.principal.type).toEqual(PrincipalType.UNKNOWN)
}
})
})
describe('Platform API Key Authentication', () => {
it('Authenticates service principals', async () => {
// arrange
const { mockPlatform, mockApiKey } =
await mockAndSaveBasicSetupWithApiKey()
const mockRequest = {
method: 'GET',
routeOptions: {
url: '/v1/flows',
config: {
allowedPrincipals: [PrincipalType.SERVICE],
scope: EndpointScope.PLATFORM,
},
},
headers: {
authorization: `Bearer ${mockApiKey.value}`,
},
} as unknown as FastifyRequest
// act
const result = securityHandlerChain(mockRequest)
// assert
await expect(result).resolves.toBeUndefined()
expect(mockRequest.principal).toEqual(
expect.objectContaining({
id: mockApiKey.id,
type: PrincipalType.SERVICE,
projectId: expect.stringMatching(/ANONYMOUS_.{21}/),
platform: {
id: mockPlatform.id,
},
}),
)
})
it('Gets projectId from body if endpoint scope is PROJECT', async () => {
// arrange
const { mockPlatform, mockProject, mockApiKey } =
await mockAndSaveBasicSetupWithApiKey()
const mockRequest = {
method: 'GET',
routeOptions: {
url: '/v1/flows',
config: {
allowedPrincipals: [PrincipalType.SERVICE],
scope: EndpointScope.PROJECT,
},
},
headers: {
authorization: `Bearer ${mockApiKey.value}`,
},
body: {
projectId: mockProject.id,
},
} as unknown as FastifyRequest
// act
const result = securityHandlerChain(mockRequest)
// assert
await expect(result).resolves.toBeUndefined()
expect(mockRequest.principal).toEqual(
expect.objectContaining({
id: mockApiKey.id,
type: PrincipalType.SERVICE,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
}),
)
})
it('Gets projectId from query if endpoint scope is PROJECT', async () => {
// arrange
const { mockPlatform, mockProject, mockApiKey } =
await mockAndSaveBasicSetupWithApiKey()
const mockRequest = {
method: 'GET',
routeOptions: {
url: '/v1/flows',
config: {
allowedPrincipals: [PrincipalType.SERVICE],
scope: EndpointScope.PROJECT,
},
},
headers: {
authorization: `Bearer ${mockApiKey.value}`,
},
query: {
projectId: mockProject.id,
},
} as unknown as FastifyRequest
// act
const result = securityHandlerChain(mockRequest)
// assert
await expect(result).resolves.toBeUndefined()
expect(mockRequest.principal).toEqual(
expect.objectContaining({
id: mockApiKey.id,
type: PrincipalType.SERVICE,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
}),
)
})
it('extracts projectId from resource if endpoint scope is PROJECT', async () => {
// arrange
const { mockPlatform, mockProject, mockApiKey } =
await mockAndSaveBasicSetupWithApiKey()
const mockFlow = createMockFlow({ projectId: mockProject.id })
await databaseConnection().getRepository('flow').save([mockFlow])
const mockRequest = {
method: 'GET',
routeOptions: {
url: '/v1/flows/:id',
config: {
allowedPrincipals: [PrincipalType.SERVICE],
scope: EndpointScope.PROJECT,
},
},
params: {
id: mockFlow.id,
},
headers: {
authorization: `Bearer ${mockApiKey.value}`,
},
} as unknown as FastifyRequest
// act
const result = securityHandlerChain(mockRequest)
// assert
await expect(result).resolves.toBeUndefined()
expect(mockRequest.principal).toEqual(
expect.objectContaining({
id: mockApiKey.id,
type: PrincipalType.SERVICE,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
}),
)
})
it('Fails if API key and project don\'t belong to same platform if endpoint scope is PROJECT', async () => {
// arrange
const { mockApiKey } = await mockAndSaveBasicSetupWithApiKey()
const { mockProject: mockOtherProject } = await mockAndSaveBasicSetup()
const mockRequest = {
method: 'GET',
routeOptions: {
url: '/v1/flows',
config: {
allowedPrincipals: [PrincipalType.SERVICE],
scope: EndpointScope.PROJECT,
},
},
query: {
projectId: mockOtherProject.id,
},
headers: {
authorization: `Bearer ${mockApiKey.value}`,
},
} as unknown as FastifyRequest
// act
const result = securityHandlerChain(mockRequest)
// assert
await expect(result).rejects.toEqual(
new ActivepiecesError({
code: ErrorCode.AUTHORIZATION,
params: {
message: 'invalid project id',
},
}),
)
})
it('Fails if no projectId is extracted from request or resource and endpoint scope is PROJECT', async () => {
// arrange
const { mockApiKey } = await mockAndSaveBasicSetupWithApiKey()
const mockRequest = {
method: 'GET',
routeOptions: {
url: '/v1/flows',
config: {
allowedPrincipals: [PrincipalType.SERVICE],
scope: EndpointScope.PROJECT,
},
},
headers: {
authorization: `Bearer ${mockApiKey.value}`,
},
} as unknown as FastifyRequest
// act
const result = securityHandlerChain(mockRequest)
// assert
await expect(result).rejects.toEqual(
new ActivepiecesError({
code: ErrorCode.VALIDATION,
params: {
message: 'invalid project id',
},
}),
)
})
it('Fails if project with extracted id doesn\'t exist and endpoint scope is PROJECT', async () => {
// arrange
const mockNonExistentProjectId = apId()
const { mockApiKey } = await mockAndSaveBasicSetupWithApiKey()
const mockRequest = {
method: 'GET',
routeOptions: {
url: '/v1/flows',
config: {
allowedPrincipals: [PrincipalType.SERVICE],
scope: EndpointScope.PROJECT,
},
},
query: {
projectId: mockNonExistentProjectId,
},
headers: {
authorization: `Bearer ${mockApiKey.value}`,
},
} as unknown as FastifyRequest
// act
const result = securityHandlerChain(mockRequest)
// assert
await expect(result).rejects.toEqual(
new ActivepiecesError({
code: ErrorCode.AUTHORIZATION,
params: {
message: 'invalid project id',
},
}),
)
})
it('Fails if API key doesn\'t exist', async () => {
// arrange
const mockNonExistentApiKey = '123'
const mockRequest = {
method: 'POST',
routeOptions: {
url: '/v1/flows',
config: {
allowedPrincipals: [PrincipalType.SERVICE],
scope: EndpointScope.PLATFORM,
},
},
headers: {
authorization: `Bearer ${mockNonExistentApiKey}`,
},
} as unknown as FastifyRequest
// act
const result = securityHandlerChain(mockRequest)
// assert
await expect(result).rejects.toEqual(
new ActivepiecesError({
code: ErrorCode.INVALID_BEARER_TOKEN,
params: {
message: 'invalid access token',
},
}),
)
})
it('Fails if route doesn\'t allow SERVICE principals', async () => {
// arrange
const { mockApiKey } = await mockAndSaveBasicSetupWithApiKey()
const mockRequest = {
method: 'POST',
routeOptions: {
url: '/v1/flows',
config: {
allowedPrincipals: [PrincipalType.USER],
scope: EndpointScope.PLATFORM,
},
},
headers: {
authorization: `Bearer ${mockApiKey.value}`,
},
} as unknown as FastifyRequest
// act
const result = securityHandlerChain(mockRequest)
// assert
await expect(result).rejects.toEqual(
new ActivepiecesError({
code: ErrorCode.AUTHORIZATION,
params: {
message: 'invalid route for principal type',
},
}),
)
})
})
describe('Access Token Authentication', () => {
it('Session expirey for Users', async () => {
// arrange
const { mockOwner, mockPlatform, mockProject, mockUserIdentity } = await mockAndSaveBasicSetupWithApiKey()
await databaseConnection().getRepository('user_identity').update(mockUserIdentity.id, {
tokenVersion: nanoid(),
})
const mockPrincipal: Principal = {
id: mockOwner.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
}
const mockAccessToken = await generateMockToken(mockPrincipal)
const mockRequest = {
method: 'GET',
routeOptions: {
url: '/v1/flows',
config: {},
},
headers: {
authorization: `Bearer ${mockAccessToken}`,
},
} as unknown as FastifyRequest
// act
const result = securityHandlerChain(mockRequest)
// assert
await expect(result).rejects.toEqual(
new ActivepiecesError({
code: ErrorCode.SESSION_EXPIRED,
params: {
message: 'The session has expired.',
},
}),
)
})
it('Authenticates users', async () => {
// arrange
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetupWithApiKey()
const mockPrincipal: Principal = {
id: mockOwner.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
}
const mockAccessToken = await generateMockToken(mockPrincipal)
const mockRequest = {
method: 'GET',
routeOptions: {
url: '/v1/flows',
config: {
allowedPrincipals: [PrincipalType.USER],
},
},
headers: {
authorization: `Bearer ${mockAccessToken}`,
},
} as unknown as FastifyRequest
// act
const result = securityHandlerChain(mockRequest)
// assert
await expect(result).resolves.toBeUndefined()
expect(mockRequest.principal).toEqual(
expect.objectContaining({
id: mockPrincipal.id,
type: PrincipalType.USER,
projectId: mockPrincipal.projectId,
platform: {
id: mockPrincipal.platform.id,
},
}),
)
})
it('Fails if route disallows USER principal type', async () => {
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetupWithApiKey()
// arrange
const mockPrincipal: Principal = {
id: mockOwner.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
}
const mockAccessToken = await generateMockToken(mockPrincipal)
const mockRequest = {
method: 'GET',
routeOptions: {
url: '/v1/flows',
config: {
allowedPrincipals: [PrincipalType.SERVICE],
},
},
headers: {
authorization: `Bearer ${mockAccessToken}`,
},
} as unknown as FastifyRequest
// act
const result = securityHandlerChain(mockRequest)
// assert
await expect(result).rejects.toEqual(
new ActivepiecesError({
code: ErrorCode.AUTHORIZATION,
params: {
message: 'invalid route for principal type',
},
}),
)
})
it('Fails if projectId in query doesn\'t match principal projectId', async () => {
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetupWithApiKey()
const mockPrincipal: Principal = {
id: mockOwner.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
}
// arrange
const mockOtherProjectId = apId()
const mockAccessToken = await generateMockToken(mockPrincipal)
const mockRequest = {
method: 'GET',
routeOptions: {
url: '/v1/flows',
config: {
allowedPrincipals: [PrincipalType.USER],
},
},
query: {
projectId: mockOtherProjectId,
},
headers: {
authorization: `Bearer ${mockAccessToken}`,
},
} as unknown as FastifyRequest
// act
const result = securityHandlerChain(mockRequest)
// assert
await expect(result).rejects.toEqual(
new ActivepiecesError({
code: ErrorCode.AUTHORIZATION,
params: {
message: 'invalid project id',
},
}),
)
})
it('Fails if projectId in body doesn\'t match principal projectId', async () => {
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetupWithApiKey()
const mockPrincipal: Principal = {
id: mockOwner.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
}
// arrange
const mockOtherProjectId = apId()
const mockAccessToken = await generateMockToken(mockPrincipal)
const mockRequest = {
method: 'GET',
routeOptions: {
url: '/v1/flows',
config: {
allowedPrincipals: [PrincipalType.USER],
},
},
headers: {
authorization: `Bearer ${mockAccessToken}`,
},
body: {
projectId: mockOtherProjectId,
},
} as unknown as FastifyRequest
// act
const result = securityHandlerChain(mockRequest)
// assert
await expect(result).rejects.toEqual(
new ActivepiecesError({
code: ErrorCode.AUTHORIZATION,
params: {
message: 'invalid project id',
},
}),
)
})
})
describe('Anonymous authentication', () => {
it('Enables access to non authenticated routes', async () => {
// arrange
const nonAuthenticatedRoute = '/v1/docs'
const mockRequest = {
method: 'GET',
routeOptions: {
url: nonAuthenticatedRoute,
config: {
allowedPrincipals: ALL_PRINCIPAL_TYPES,
},
},
headers: {},
} as unknown as FastifyRequest
// act
const result = securityHandlerChain(mockRequest)
// assert
await expect(result).resolves.toBeUndefined()
expect(mockRequest.principal).toEqual(
expect.objectContaining({
id: expect.stringMatching(/ANONYMOUS_.{21}/),
type: PrincipalType.UNKNOWN,
}),
)
})
it('Fails if route is authenticated', async () => {
// arrange
const authenticatedRoute = '/v1/flows'
const mockRequest = {
method: 'GET',
routeOptions: {
url: authenticatedRoute,
config: {
allowedPrincipals: [PrincipalType.USER],
},
},
headers: {},
} as unknown as FastifyRequest
// act
const result = securityHandlerChain(mockRequest)
// assert
await expect(result).rejects.toEqual(
new ActivepiecesError({
code: ErrorCode.AUTHORIZATION,
params: {
message: 'invalid route for principal type',
},
}),
)
})
})
})

View File

@@ -0,0 +1,224 @@
import { AddDomainRequest, CustomDomainStatus } from '@activepieces/ee-shared'
import { PlatformRole, PrincipalType } from '@activepieces/shared'
import { faker } from '@faker-js/faker'
import { FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
import { generateMockToken } from '../../../helpers/auth'
import {
createMockCustomDomain,
mockAndSaveBasicSetup,
mockBasicUser } from '../../../helpers/mocks'
let app: FastifyInstance | null = null
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('Custom Domain API', () => {
describe('Add Custom Domain API', () => {
it('should create a new custom domain', async () => {
// arrange
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockOwner.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
const request: AddDomainRequest = {
domain: faker.internet.domainName(),
}
const response = await app?.inject({
method: 'POST',
url: '/v1/custom-domains',
body: request,
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.CREATED)
const responseBody = response?.json()
expect(responseBody.domain).toBe(request.domain)
expect(responseBody.status).toBe(CustomDomainStatus.PENDING)
})
it('should fail if user is not platform owner', async () => {
// arrange
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const { mockUser: nonOwnerUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: nonOwnerUser.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
const request: AddDomainRequest = {
domain: faker.internet.domainName(),
}
const response = await app?.inject({
method: 'POST',
url: '/v1/custom-domains',
body: request,
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
})
describe('List Custom Domain API', () => {
it('should list custom domains', async () => {
// arrange
const { mockOwner: mockUserOne, mockPlatform: mockPlatformOne, mockProject: mockProjectOne } = await mockAndSaveBasicSetup()
const { mockPlatform: mockPlatformTwo } = await mockAndSaveBasicSetup()
const testToken1 = await generateMockToken({
type: PrincipalType.USER,
id: mockUserOne.id,
projectId: mockProjectOne.id,
platform: { id: mockPlatformOne.id },
})
const mockCustomDomains1 = [
createMockCustomDomain({
platformId: mockPlatformOne.id,
domain: faker.internet.domainName(),
}),
createMockCustomDomain({
platformId: mockPlatformOne.id,
domain: faker.internet.domainName(),
}),
]
await databaseConnection()
.getRepository('custom_domain')
.save(mockCustomDomains1)
const mockCustomDomains2 = [
createMockCustomDomain({
platformId: mockPlatformTwo.id,
domain: faker.internet.domainName(),
}),
]
await databaseConnection()
.getRepository('custom_domain')
.save(mockCustomDomains2)
// act
const response1 = await app?.inject({
method: 'GET',
url: '/v1/custom-domains',
headers: {
authorization: `Bearer ${testToken1}`,
},
})
// assert
expect(response1?.statusCode).toBe(StatusCodes.OK)
const responseBody1 = response1?.json()
expect(responseBody1.data).toHaveLength(mockCustomDomains1.length)
expect(responseBody1?.data).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: mockCustomDomains1[0].id }),
expect.objectContaining({ id: mockCustomDomains1[1].id }),
]),
)
})
})
describe('Delete Custom Domain API', () => {
it('should delete a custom domain', async () => {
// arrange
const { mockOwner: mockUserOne, mockPlatform: mockPlatformOne, mockProject: mockProjectOne } = await mockAndSaveBasicSetup()
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockUserOne.id,
projectId: mockProjectOne.id,
platform: { id: mockPlatformOne.id },
})
const customDomain = createMockCustomDomain({
platformId: mockPlatformOne.id,
domain: faker.internet.domainName(),
})
await databaseConnection()
.getRepository('custom_domain')
.save(customDomain)
// act
const response = await app?.inject({
method: 'DELETE',
url: `/v1/custom-domains/${customDomain.id}`,
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
})
it('should fail to delete a custom domain if user is not platform owner', async () => {
const { mockPlatform: mockPlatformOne, mockProject: mockProjectOne } = await mockAndSaveBasicSetup()
const { mockUser: nonOwnerUser } = await mockBasicUser({
user: {
platformId: mockPlatformOne.id,
platformRole: PlatformRole.MEMBER,
},
})
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: nonOwnerUser.id,
projectId: mockProjectOne.id,
platform: { id: mockPlatformOne.id },
})
const customDomain = createMockCustomDomain({
platformId: mockPlatformOne.id,
domain: faker.internet.domainName(),
})
await databaseConnection()
.getRepository('custom_domain')
.save(customDomain)
// act
const response = await app?.inject({
method: 'DELETE',
url: `/v1/custom-domains/${customDomain.id}`,
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
})
})

View File

@@ -0,0 +1,226 @@
import {
apId,
CreateTemplateRequestBody,
PlatformPlan,
PlatformRole,
PrincipalType,
TemplateType,
} from '@activepieces/shared'
import { FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
import { generateMockToken } from '../../../helpers/auth'
import {
CLOUD_PLATFORM_ID,
createMockTemplate,
mockAndSaveBasicSetup,
mockBasicUser,
} from '../../../helpers/mocks'
let app: FastifyInstance | null = null
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('Templates', () => {
describe('List Templates', () => {
it('should list platform templates only', async () => {
// arrange
const { mockPlatform, mockUser, mockProject, mockPlatformTemplate } =
await createMockPlatformTemplate({ platformId: apId(), plan: { manageTemplatesEnabled: true } })
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockUser.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
const response = await app?.inject({
method: 'GET',
url: '/v1/templates',
headers: {
authorization: `Bearer ${testToken}`,
},
query: {
type: TemplateType.CUSTOM,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody.data).toHaveLength(1)
expect(responseBody.data[0].id).toBe(mockPlatformTemplate.id)
})
it('should list cloud platform template for anonymous users', async () => {
const response = await app?.inject({
method: 'GET',
url: '/v1/templates',
query: {
type: TemplateType.OFFICIAL,
},
})
expect(response?.statusCode).toBe(StatusCodes.OK)
})
})
describe('Create Template', () => {
it('should create a flow template', async () => {
// arrange
const { mockPlatform, mockOwner, mockProject } = await mockAndSaveBasicSetup({
platform: {
},
plan: {
manageTemplatesEnabled: true,
},
})
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockOwner.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
const mockTemplate = createMockTemplate({
platformId: mockPlatform.id,
type: TemplateType.CUSTOM,
})
const createTemplateRequest: CreateTemplateRequestBody = {
name: mockTemplate.name,
description: mockTemplate.description,
summary: mockTemplate.summary,
flows: mockTemplate.flows,
blogUrl: mockTemplate.blogUrl ?? undefined,
type: TemplateType.CUSTOM,
author: mockTemplate.author,
categories: mockTemplate.categories,
tags: mockTemplate.tags,
metadata: {
foo: 'bar',
},
}
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/templates',
headers: {
authorization: `Bearer ${testToken}`,
},
body: createTemplateRequest,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.CREATED)
const responseBody = response?.json()
expect(responseBody.metadata).toEqual({ foo: 'bar' })
})
})
describe('Delete Template', () => {
it('should not be able delete platform template as member', async () => {
// arrange
const { mockUser, mockPlatform, mockProject, mockPlatformTemplate } =
await createMockPlatformTemplate({ platformId: apId() })
const testToken = await generateMockToken({
id: mockUser.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
const response = await app?.inject({
method: 'DELETE',
url: `/v1/templates/${mockPlatformTemplate.id}`,
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
it('should be able delete platform template as owner', async () => {
// arrange
const { mockPlatform, mockOwner, mockProject, mockPlatformTemplate } =
await createMockPlatformTemplate({ platformId: apId() })
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockOwner.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
const response = await app?.inject({
method: 'DELETE',
url: `/v1/templates/${mockPlatformTemplate.id}`,
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.NO_CONTENT)
})
it('should not delete platform template when not authenticated', async () => {
// arrange
const { mockPlatformTemplate } = await createMockPlatformTemplate({
platformId: CLOUD_PLATFORM_ID,
})
const response = await app?.inject({
method: 'DELETE',
url: `/v1/templates/${mockPlatformTemplate.id}`,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
})
})
async function createMockPlatformTemplate({ platformId, plan, type }: { platformId: string, plan?: Partial<PlatformPlan>, type?: TemplateType }) {
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup({
platform: {
id: platformId,
},
plan: {
manageTemplatesEnabled: true,
...plan,
},
})
const mockPlatformTemplate = createMockTemplate({
platformId: mockPlatform.id,
type: type ?? TemplateType.CUSTOM,
})
await databaseConnection()
.getRepository('template')
.save(mockPlatformTemplate)
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
return { mockOwner, mockUser, mockPlatform, mockProject, mockPlatformTemplate }
}

View File

@@ -0,0 +1,418 @@
import {
DefaultProjectRole,
FlowOperationType,
FlowStatus,
FlowTriggerType,
PackageType,
PieceType,
PlatformRole,
PrincipalType,
ProjectRole,
TriggerStrategy,
TriggerTestStrategy,
} from '@activepieces/shared'
import { FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
import { generateMockToken } from '../../../helpers/auth'
import {
createMockFlow,
createMockFlowVersion,
createMockPieceMetadata,
createMockProjectMember,
mockAndSaveBasicSetup,
mockBasicUser,
} from '../../../helpers/mocks'
let app: FastifyInstance | null = null
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('Flow API', () => {
describe('Create Flow endpoint', () => {
it.each([
DefaultProjectRole.ADMIN,
DefaultProjectRole.EDITOR,
])('Succeeds if user role is %s', async (testRole) => {
// arrange
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: testRole }) as ProjectRole
const mockProjectMember = createMockProjectMember({
userId: mockUser.id,
platformId: mockPlatform.id,
projectId: mockProject.id,
projectRoleId: projectRole.id,
})
await databaseConnection().getRepository('project_member').save([mockProjectMember])
const mockToken = await generateMockToken({
id: mockUser.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
const mockCreateFlowRequest = {
displayName: 'test flow',
projectId: mockProject.id,
}
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/flows',
headers: {
authorization: `Bearer ${mockToken}`,
},
body: mockCreateFlowRequest,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.CREATED)
})
it.each([
DefaultProjectRole.VIEWER,
DefaultProjectRole.OPERATOR,
])('Fails if user role is %s', async (testRole) => {
// arrange
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: testRole }) as ProjectRole
const mockProjectMember = createMockProjectMember({
userId: mockUser.id,
platformId: mockPlatform.id,
projectId: mockProject.id,
projectRoleId: projectRole.id,
})
await databaseConnection().getRepository('project_member').save([mockProjectMember])
const mockToken = await generateMockToken({
id: mockUser.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
const mockCreateFlowRequest = {
displayName: 'test flow',
projectId: mockProject.id,
}
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/flows',
headers: {
authorization: `Bearer ${mockToken}`,
},
body: mockCreateFlowRequest,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
const responseBody = response?.json()
expect(responseBody?.code).toBe('PERMISSION_DENIED')
expect(responseBody?.params?.userId).toBe(mockUser.id)
expect(responseBody?.params?.projectId).toBe(mockProject.id)
})
})
describe('Update flow endpoint', () => {
it.each([
{
role: DefaultProjectRole.ADMIN,
request: {
type: FlowOperationType.CHANGE_STATUS,
request: {
status: 'ENABLED',
},
},
},
{
role: DefaultProjectRole.EDITOR,
request: {
type: FlowOperationType.CHANGE_STATUS,
request: {
status: 'ENABLED',
},
},
},
{
role: DefaultProjectRole.OPERATOR,
request: {
type: FlowOperationType.CHANGE_STATUS,
request: {
status: 'ENABLED',
},
},
},
])('Succeeds if user role is %s', async ({ role, request }) => {
// arrange
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: role }) as ProjectRole
const mockProjectMember = createMockProjectMember({
userId: mockUser.id,
platformId: mockPlatform.id,
projectId: mockProject.id,
projectRoleId: projectRole.id,
})
await databaseConnection().getRepository('project_member').save([mockProjectMember])
const mockFlow = createMockFlow({
projectId: mockProject.id,
status: FlowStatus.DISABLED,
})
await databaseConnection().getRepository('flow').save([mockFlow])
const mockPieceMetadata = createMockPieceMetadata({
name: '@activepieces/piece-schedule',
version: '0.1.5',
triggers: {
'every_hour': {
'name': 'every_hour',
'displayName': 'Every Hour',
'description': 'Triggers the current flow every hour',
'requireAuth': false,
'props': {
},
'type': TriggerStrategy.POLLING,
'sampleData': {
},
'testStrategy': TriggerTestStrategy.TEST_FUNCTION,
},
},
pieceType: PieceType.OFFICIAL,
packageType: PackageType.REGISTRY,
})
await databaseConnection().getRepository('piece_metadata').save([mockPieceMetadata])
const mockFlowVersion = createMockFlowVersion({
flowId: mockFlow.id,
updatedBy: mockUser.id,
trigger: {
type: FlowTriggerType.PIECE,
name: 'trigger',
settings: {
pieceName: '@activepieces/piece-schedule',
pieceVersion: '0.1.5',
input: {},
propertySettings: {},
triggerName: 'every_hour',
},
valid: true,
displayName: 'Trigger',
},
})
await databaseConnection()
.getRepository('flow_version')
.save([mockFlowVersion])
await databaseConnection().getRepository('flow').update(mockFlow.id, {
publishedVersionId: mockFlowVersion.id,
})
const mockToken = await generateMockToken({
id: mockUser.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
// act
const response = await app?.inject({
method: 'POST',
url: `/v1/flows/${mockFlow.id}`,
headers: {
authorization: `Bearer ${mockToken}`,
},
body: request,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
})
it.each([
{
role: DefaultProjectRole.VIEWER,
request: {
type: FlowOperationType.CHANGE_STATUS,
request: {
status: 'ENABLED',
},
},
},
{
role: DefaultProjectRole.OPERATOR,
request: {
type: FlowOperationType.CHANGE_NAME,
request: {
displayName: 'hello',
},
},
},
])('Fails if user role is %s', async ({ role, request }) => {
// arrange
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: role }) as ProjectRole
const mockProjectMember = createMockProjectMember({
userId: mockUser.id,
platformId: mockPlatform.id,
projectId: mockProject.id,
projectRoleId: projectRole.id,
})
await databaseConnection().getRepository('project_member').save([mockProjectMember])
const mockFlow = createMockFlow({
projectId: mockProject.id,
status: FlowStatus.DISABLED,
})
await databaseConnection().getRepository('flow').save([mockFlow])
const mockFlowVersion = createMockFlowVersion({
flowId: mockFlow.id,
updatedBy: mockUser.id,
})
await databaseConnection()
.getRepository('flow_version')
.save([mockFlowVersion])
await databaseConnection().getRepository('flow').update(mockFlow.id, {
publishedVersionId: mockFlowVersion.id,
})
const mockToken = await generateMockToken({
id: mockUser.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
// act
const response = await app?.inject({
method: 'POST',
url: `/v1/flows/${mockFlow.id}`,
headers: {
authorization: `Bearer ${mockToken}`,
},
body: request,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
const responseBody = response?.json()
expect(responseBody?.code).toBe('PERMISSION_DENIED')
expect(responseBody?.params?.userId).toBe(mockUser.id)
expect(responseBody?.params?.projectId).toBe(mockProject.id)
})
})
describe('List Flows endpoint', () => {
it.each([
DefaultProjectRole.ADMIN,
DefaultProjectRole.EDITOR,
DefaultProjectRole.OPERATOR,
DefaultProjectRole.VIEWER,
])('Succeeds if user role is %s', async (testRole) => {
// arrange
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: testRole }) as ProjectRole
const mockProjectMember = createMockProjectMember({
userId: mockUser.id,
platformId: mockPlatform.id,
projectId: mockProject.id,
projectRoleId: projectRole.id,
})
await databaseConnection().getRepository('project_member').save([mockProjectMember])
const mockToken = await generateMockToken({
id: mockUser.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
// act
const response = await app?.inject({
method: 'GET',
url: '/v1/flows',
query: {
projectId: mockProject.id,
status: 'ENABLED',
},
headers: {
authorization: `Bearer ${mockToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
})
})
})

View File

@@ -0,0 +1,333 @@
import { GitBranchType } from '@activepieces/ee-shared'
import { PrincipalType } from '@activepieces/shared'
import { faker } from '@faker-js/faker'
import { FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
import { generateMockToken } from '../../../helpers/auth'
import {
createMockGitRepo,
createMockProject,
mockAndSaveBasicSetup,
mockAndSaveBasicSetupWithApiKey,
mockBasicUser,
} from '../../../helpers/mocks'
let app: FastifyInstance | null = null
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('Git API', () => {
describe('Create API', () => {
it('should not allow create git repo for other projects', async () => {
const { mockPlatform, mockProject, mockOwner } = await mockAndSaveBasicSetup({
platform: {
},
plan: {
environmentsEnabled: true,
},
})
const { mockUser: mockUser2 } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
},
})
const mockProject2 = createMockProject({ platformId: mockPlatform.id, ownerId: mockUser2.id })
await databaseConnection().getRepository('project').save(mockProject2)
const request = {
projectId: mockProject2.id,
remoteUrl: `git@${faker.internet.url()}`,
sshPrivateKey: faker.hacker.noun(),
branch: 'main',
branchType: GitBranchType.PRODUCTION,
slug: 'test-slug',
}
const token = await generateMockToken({
id: mockOwner.id,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
type: PrincipalType.USER,
})
const response = await app?.inject({
method: 'POST',
url: '/v1/git-repos',
payload: request,
headers: {
authorization: `Bearer ${token}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
it('should create a git repo', async () => {
const { mockProject, mockOwner } = await mockAndSaveBasicSetup({
platform: {
},
plan: {
environmentsEnabled: true,
},
})
const request = {
projectId: mockProject.id,
remoteUrl: `git@${faker.internet.url()}`,
sshPrivateKey: faker.hacker.noun(),
branch: 'main',
branchType: GitBranchType.PRODUCTION,
slug: 'test-slug',
}
const token = await generateMockToken({
id: mockOwner.id,
projectId: mockProject.id,
type: PrincipalType.USER,
platform: {
id: mockProject.platformId,
},
})
const response = await app?.inject({
method: 'POST',
url: '/v1/git-repos',
payload: request,
headers: {
authorization: `Bearer ${token}`,
},
})
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.CREATED)
expect(responseBody.sshPrivateKey).toBeUndefined()
expect(responseBody.remoteUrl).toBe(request.remoteUrl)
expect(responseBody.branch).toBe(request.branch)
expect(responseBody.created).toBeDefined()
expect(responseBody.updated).toBeDefined()
expect(responseBody.id).toBeDefined()
expect(responseBody.projectId).toBe(mockProject.id)
expect(responseBody.slug).toBe('test-slug')
})
})
describe('Delete API', () => {
it('should delete a git repo', async () => {
const { mockProject, mockOwner } = await mockAndSaveBasicSetup({
platform: {
},
plan: {
environmentsEnabled: true,
},
})
const mockGitRepo = createMockGitRepo({ projectId: mockProject.id })
await databaseConnection().getRepository('git_repo').save(mockGitRepo)
const token = await generateMockToken({
id: mockOwner.id,
projectId: mockProject.id,
type: PrincipalType.USER,
platform: {
id: mockProject.platformId,
},
})
const response = await app?.inject({
method: 'DELETE',
url: '/v1/git-repos/' + mockGitRepo.id,
headers: {
authorization: `Bearer ${token}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.NO_CONTENT)
})
it('should not allow delete git repo for other projects', async () => {
const { mockPlatform, mockProject, mockOwner } = await mockAndSaveBasicSetup({
platform: {
},
plan: {
environmentsEnabled: true,
},
})
const mockProject2 = createMockProject({ platformId: mockPlatform.id, ownerId: mockOwner.id })
await databaseConnection().getRepository('project').save(mockProject2)
const mockGitRepo = createMockGitRepo({ projectId: mockProject.id })
const mockGitRepo2 = createMockGitRepo({ projectId: mockProject2.id })
await databaseConnection()
.getRepository('git_repo')
.save([mockGitRepo, mockGitRepo2])
const token = await generateMockToken({
id: mockOwner.id,
projectId: mockProject.id,
type: PrincipalType.USER,
platform: {
id: mockProject.platformId,
},
})
const response = await app?.inject({
method: 'DELETE',
url: '/v1/git-repos/' + mockGitRepo2.id,
headers: {
authorization: `Bearer ${token}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.NOT_FOUND)
})
})
describe('List API', () => {
it('should list return forbidden when api request wrong project', async () => {
const { mockPlatform, mockProject, mockApiKey, mockOwner } = await mockAndSaveBasicSetupWithApiKey({
platform: {
},
plan: {
environmentsEnabled: true,
},
})
const { mockProject: mockProject3 } = await mockAndSaveBasicSetup({
platform: {
},
plan: {
environmentsEnabled: true,
},
})
const mockProject2 = createMockProject({ platformId: mockPlatform.id, ownerId: mockOwner.id })
await databaseConnection()
.getRepository('project')
.save([mockProject2])
const mockGitRepo = createMockGitRepo({ projectId: mockProject.id })
const mockGitRepo2 = createMockGitRepo({ projectId: mockProject2.id })
await databaseConnection()
.getRepository('git_repo')
.save([mockGitRepo, mockGitRepo2])
const response = await app?.inject({
method: 'GET',
url: '/v1/git-repos?projectId=' + mockProject3.id,
headers: {
authorization: `Bearer ${mockApiKey.value}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
it('should list return forbidden when user request wrong project', async () => {
const { mockPlatform, mockProject, mockOwner } = await mockAndSaveBasicSetup({
platform: {
},
plan: {
environmentsEnabled: true,
},
})
const { mockProject: mockProject3 } = await mockAndSaveBasicSetup({
platform: {
},
plan: {
environmentsEnabled: true,
},
})
const mockProject2 = createMockProject({ platformId: mockPlatform.id, ownerId: mockOwner.id })
await databaseConnection()
.getRepository('project')
.save([mockProject2])
const mockGitRepo = createMockGitRepo({ projectId: mockProject.id })
const mockGitRepo2 = createMockGitRepo({ projectId: mockProject2.id })
await databaseConnection()
.getRepository('git_repo')
.save([mockGitRepo, mockGitRepo2])
const token = await generateMockToken({
id: mockOwner.id,
projectId: mockProject.id,
type: PrincipalType.USER,
platform: {
id: mockProject.platformId,
},
})
const response = await app?.inject({
method: 'GET',
url: '/v1/git-repos?projectId=' + mockProject3.id,
headers: {
authorization: `Bearer ${token}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
it('should list a git repo', async () => {
const { mockPlatform, mockProject, mockOwner } = await mockAndSaveBasicSetup({
platform: {
},
plan: {
environmentsEnabled: true,
},
})
const mockProject2 = createMockProject({ platformId: mockPlatform.id, ownerId: mockOwner.id })
await databaseConnection()
.getRepository('project')
.save([mockProject2])
const mockGitRepo = createMockGitRepo({ projectId: mockProject.id })
const mockGitRepo2 = createMockGitRepo({ projectId: mockProject2.id })
await databaseConnection()
.getRepository('git_repo')
.save([mockGitRepo, mockGitRepo2])
const token = await generateMockToken({
id: mockOwner.id,
projectId: mockProject.id,
type: PrincipalType.USER,
platform: {
id: mockProject.platformId,
},
})
const response = await app?.inject({
method: 'GET',
url: '/v1/git-repos?projectId=' + mockProject.id,
headers: {
authorization: `Bearer ${token}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.OK)
const responseBody = response?.json()
expect(responseBody.data.length).toBe(1)
const gitRepo = responseBody.data[0]
expect(gitRepo.sshPrivateKey).toBeUndefined()
expect(gitRepo.remoteUrl).toBe(mockGitRepo.remoteUrl)
expect(gitRepo.branch).toBe(mockGitRepo.branch)
expect(gitRepo.created).toBeDefined()
expect(gitRepo.updated).toBeDefined()
expect(gitRepo.id).toBeDefined()
expect(gitRepo.projectId).toBe(mockProject.id)
expect(gitRepo.slug).toBe(mockGitRepo.slug)
})
})
})

View File

@@ -0,0 +1,606 @@
import {
apId,
AppConnectionScope,
AppConnectionType,
PackageType,
PlatformRole,
PrincipalType,
UpdateGlobalConnectionValueRequestBody,
UpsertGlobalConnectionRequestBody,
} from '@activepieces/shared'
import { FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
import { generateMockToken } from '../../../helpers/auth'
import {
createMockPieceMetadata,
mockAndSaveBasicSetup,
mockBasicUser,
} from '../../../helpers/mocks'
let app: FastifyInstance | null = null
beforeAll(async () => {
await databaseConnection().initialize()
app = await setupServer()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
const setupWithGlobalConnections = () => {
return mockAndSaveBasicSetup({
platform: {
},
plan: {
globalConnectionsEnabled: true,
},
})
}
describe('GlobalConnection API', () => {
describe('Upsert GlobalConnection endpoint', () => {
it('Succeeds if user is platform owner', async () => {
// arrange
const { mockPlatform, mockProject, mockOwner } = await setupWithGlobalConnections()
const mockPieceMetadata = createMockPieceMetadata({
projectId: mockProject.id,
platformId: mockPlatform.id,
packageType: PackageType.REGISTRY,
})
await databaseConnection().getRepository('piece_metadata').save([mockPieceMetadata])
const mockToken = await generateMockToken({
id: mockOwner.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
const mockUpsertGlobalConnectionRequest: UpsertGlobalConnectionRequestBody = {
pieceVersion: mockPieceMetadata.version,
displayName: 'test global connection',
pieceName: mockPieceMetadata.name,
projectIds: [mockProject.id],
scope: AppConnectionScope.PLATFORM,
type: AppConnectionType.SECRET_TEXT,
value: {
type: AppConnectionType.SECRET_TEXT,
secret_text: 'test-secret-text',
},
}
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/global-connections',
headers: {
authorization: `Bearer ${mockToken}`,
},
body: mockUpsertGlobalConnectionRequest,
})
const responseBody = response?.json()
expect(responseBody.pieceVersion).toEqual(mockPieceMetadata.version)
// assert
expect(response?.statusCode).toBe(StatusCodes.CREATED)
})
it('Fails if user is not platform owner', async () => {
// arrange
const { mockPlatform, mockProject } = await setupWithGlobalConnections()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const mockPieceMetadata = createMockPieceMetadata({
projectId: mockProject.id,
platformId: mockPlatform.id,
})
await databaseConnection().getRepository('piece_metadata').save([mockPieceMetadata])
const mockToken = await generateMockToken({
id: mockUser.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
const mockUpsertGlobalConnectionRequest: UpsertGlobalConnectionRequestBody = {
pieceVersion: mockPieceMetadata.version,
displayName: 'test global connection',
pieceName: mockPieceMetadata.name,
scope: AppConnectionScope.PLATFORM,
projectIds: [mockProject.id],
type: AppConnectionType.SECRET_TEXT,
value: {
type: AppConnectionType.SECRET_TEXT,
secret_text: 'test-secret-text',
},
}
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/global-connections',
headers: {
authorization: `Bearer ${mockToken}`,
},
body: mockUpsertGlobalConnectionRequest,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
it('Fails if project ids are invalid', async () => {
// arrange
const { mockPlatform, mockProject, mockOwner } = await setupWithGlobalConnections()
const mockPieceMetadata = createMockPieceMetadata({
projectId: mockProject.id,
platformId: mockPlatform.id,
packageType: PackageType.REGISTRY,
})
await databaseConnection().getRepository('piece_metadata').save([mockPieceMetadata])
const mockToken = await generateMockToken({
id: mockOwner.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
const mockUpsertGlobalConnectionRequest: UpsertGlobalConnectionRequestBody = {
pieceVersion: mockPieceMetadata.version,
displayName: 'test global connection',
pieceName: mockPieceMetadata.name,
projectIds: [apId()], // Invalid project ID
scope: AppConnectionScope.PLATFORM,
type: AppConnectionType.SECRET_TEXT,
value: {
type: AppConnectionType.SECRET_TEXT,
secret_text: 'test-secret-text',
},
}
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/global-connections',
headers: {
authorization: `Bearer ${mockToken}`,
},
body: mockUpsertGlobalConnectionRequest,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.NOT_FOUND)
})
})
describe('List GlobalConnections endpoint', () => {
it('Succeeds if user is platform owner', async () => {
// arrange
const { mockPlatform, mockProject, mockOwner } = await setupWithGlobalConnections()
const mockToken = await generateMockToken({
id: mockOwner.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
// act
const response = await app?.inject({
method: 'GET',
url: '/v1/global-connections',
headers: {
authorization: `Bearer ${mockToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
})
it('Fails if user is not platform owner', async () => {
// arrange
const { mockPlatform, mockProject } = await setupWithGlobalConnections()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const mockToken = await generateMockToken({
id: mockUser.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
// act
const response = await app?.inject({
method: 'GET',
url: '/v1/global-connections',
headers: {
authorization: `Bearer ${mockToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
})
describe('Delete GlobalConnection endpoint', () => {
it('Succeeds if user is platform owner', async () => {
// arrange
const { mockPlatform, mockProject, mockOwner } = await setupWithGlobalConnections()
const mockPieceMetadata = createMockPieceMetadata({
projectId: mockProject.id,
platformId: mockPlatform.id,
packageType: PackageType.REGISTRY,
})
await databaseConnection().getRepository('piece_metadata').save([mockPieceMetadata])
const mockToken = await generateMockToken({
id: mockOwner.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
const mockUpsertGlobalConnectionRequest: UpsertGlobalConnectionRequestBody = {
pieceVersion: mockPieceMetadata.version,
displayName: 'test global connection',
pieceName: mockPieceMetadata.name,
scope: AppConnectionScope.PLATFORM,
projectIds: [mockProject.id],
type: AppConnectionType.SECRET_TEXT,
value: {
type: AppConnectionType.SECRET_TEXT,
secret_text: 'test-secret-text',
},
}
const upsertResponse = await app?.inject({
method: 'POST',
url: '/v1/global-connections',
headers: {
authorization: `Bearer ${mockToken}`,
},
body: mockUpsertGlobalConnectionRequest,
})
const connectionId = upsertResponse?.json().id
// act
const response = await app?.inject({
method: 'DELETE',
url: `/v1/global-connections/${connectionId}`,
headers: {
authorization: `Bearer ${mockToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.NO_CONTENT)
})
it('Fails if user is not platform owner', async () => {
// arrange
const { mockPlatform, mockProject, mockOwner } = await setupWithGlobalConnections()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const mockPieceMetadata = createMockPieceMetadata({
projectId: mockProject.id,
platformId: mockPlatform.id,
packageType: PackageType.REGISTRY,
})
await databaseConnection().getRepository('piece_metadata').save([mockPieceMetadata])
const mockOwnerToken = await generateMockToken({
id: mockOwner.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
const mockUpsertGlobalConnectionRequest: UpsertGlobalConnectionRequestBody = {
pieceVersion: mockPieceMetadata.version,
displayName: 'test global connection',
pieceName: mockPieceMetadata.name,
scope: AppConnectionScope.PLATFORM,
projectIds: [mockProject.id],
type: AppConnectionType.SECRET_TEXT,
value: {
type: AppConnectionType.SECRET_TEXT,
secret_text: 'test-secret-text',
},
}
const upsertResponse = await app?.inject({
method: 'POST',
url: '/v1/global-connections',
headers: {
authorization: `Bearer ${mockOwnerToken}`,
},
body: mockUpsertGlobalConnectionRequest,
})
const connectionId = upsertResponse?.json().id
const mockUserToken = await generateMockToken({
id: mockUser.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
// act
const response = await app?.inject({
method: 'DELETE',
url: `/v1/global-connections/${connectionId}`,
headers: {
authorization: `Bearer ${mockUserToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
})
describe('Update GlobalConnection endpoint', () => {
it('Succeeds if user is platform owner', async () => {
// arrange
const { mockPlatform, mockProject, mockOwner } = await setupWithGlobalConnections()
const mockPieceMetadata = createMockPieceMetadata({
projectId: mockProject.id,
platformId: mockPlatform.id,
packageType: PackageType.REGISTRY,
})
await databaseConnection().getRepository('piece_metadata').save([mockPieceMetadata])
const mockToken = await generateMockToken({
id: mockOwner.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
const mockUpsertGlobalConnectionRequest: UpsertGlobalConnectionRequestBody = {
pieceVersion: mockPieceMetadata.version,
displayName: 'test global connection',
pieceName: mockPieceMetadata.name,
scope: AppConnectionScope.PLATFORM,
type: AppConnectionType.SECRET_TEXT,
projectIds: [mockProject.id],
value: {
type: AppConnectionType.SECRET_TEXT,
secret_text: 'test-secret-text',
},
}
const upsertResponse = await app?.inject({
method: 'POST',
url: '/v1/global-connections',
headers: {
authorization: `Bearer ${mockToken}`,
},
body: mockUpsertGlobalConnectionRequest,
})
const connectionId = upsertResponse?.json().id
const mockUpdateGlobalConnectionRequest: UpdateGlobalConnectionValueRequestBody = {
displayName: 'updated-global-connection',
}
// act
const response = await app?.inject({
method: 'POST',
url: `/v1/global-connections/${connectionId}`,
headers: {
authorization: `Bearer ${mockToken}`,
},
body: mockUpdateGlobalConnectionRequest,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(response?.json().displayName).toBe('updated-global-connection')
})
it('Fails if user is not platform owner', async () => {
// arrange
const { mockPlatform, mockProject, mockOwner } = await setupWithGlobalConnections()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const mockPieceMetadata = createMockPieceMetadata({
projectId: mockProject.id,
platformId: mockPlatform.id,
packageType: PackageType.REGISTRY,
})
await databaseConnection().getRepository('piece_metadata').save([mockPieceMetadata])
const mockOwnerToken = await generateMockToken({
id: mockOwner.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
const mockUpsertGlobalConnectionRequest: UpsertGlobalConnectionRequestBody = {
pieceVersion: mockPieceMetadata.version,
displayName: 'test global connection',
pieceName: mockPieceMetadata.name,
scope: AppConnectionScope.PLATFORM,
type: AppConnectionType.SECRET_TEXT,
projectIds: [mockProject.id],
value: {
type: AppConnectionType.SECRET_TEXT,
secret_text: 'test-secret-text',
},
}
const upsertResponse = await app?.inject({
method: 'POST',
url: '/v1/global-connections',
headers: {
authorization: `Bearer ${mockOwnerToken}`,
},
body: mockUpsertGlobalConnectionRequest,
})
const connectionId = upsertResponse?.json().id
const mockUserToken = await generateMockToken({
id: mockUser.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
const mockUpdateGlobalConnectionRequest = {
displayName: 'updated-global-connection',
}
// act
const response = await app?.inject({
method: 'POST',
url: `/v1/global-connections/${connectionId}`,
headers: {
authorization: `Bearer ${mockUserToken}`,
},
body: mockUpdateGlobalConnectionRequest,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
it('Fails if project ids are invalid', async () => {
// arrange
const { mockPlatform, mockProject, mockOwner } = await setupWithGlobalConnections()
const mockPieceMetadata = createMockPieceMetadata({
projectId: mockProject.id,
platformId: mockPlatform.id,
packageType: PackageType.REGISTRY,
})
await databaseConnection().getRepository('piece_metadata').save([mockPieceMetadata])
const mockToken = await generateMockToken({
id: mockOwner.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
const mockUpsertGlobalConnectionRequest: UpsertGlobalConnectionRequestBody = {
pieceVersion: mockPieceMetadata.version,
displayName: 'test global connection',
pieceName: mockPieceMetadata.name,
scope: AppConnectionScope.PLATFORM,
type: AppConnectionType.SECRET_TEXT,
projectIds: [mockProject.id],
value: {
type: AppConnectionType.SECRET_TEXT,
secret_text: 'test-secret-text',
},
}
const upsertResponse = await app?.inject({
method: 'POST',
url: '/v1/global-connections',
headers: {
authorization: `Bearer ${mockToken}`,
},
body: mockUpsertGlobalConnectionRequest,
})
const connectionId = upsertResponse?.json().id
const mockUpdateGlobalConnectionRequest: UpdateGlobalConnectionValueRequestBody = {
projectIds: [apId()], // Invalid project ID
displayName: 'updated-global-connection',
}
// act
const response = await app?.inject({
method: 'POST',
url: `/v1/global-connections/${connectionId}`,
headers: {
authorization: `Bearer ${mockToken}`,
},
body: mockUpdateGlobalConnectionRequest,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.NOT_FOUND)
})
})
})

View File

@@ -0,0 +1,365 @@
import { apId, DefaultProjectRole, PiecesFilterType, PieceType, ProjectRole } from '@activepieces/shared'
import { FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
import { generateMockExternalToken } from '../../../helpers/auth'
import {
createMockPieceMetadata,
createMockPieceTag,
createMockProject,
createMockSigningKey,
createMockTag,
mockAndSaveBasicSetup,
mockBasicUser,
} from '../../../helpers/mocks'
let app: FastifyInstance | null = null
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('Managed Authentication API', () => {
describe('External token endpoint', () => {
it('Signs up new users', async () => {
// arrange
const { mockPlatform } = await mockAndSaveBasicSetup()
const mockSigningKey = createMockSigningKey({
platformId: mockPlatform.id,
})
await databaseConnection()
.getRepository('signing_key')
.save(mockSigningKey)
const { mockExternalToken, mockExternalTokenPayload } = generateMockExternalToken({
platformId: mockPlatform.id,
signingKeyId: mockSigningKey.id,
})
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/managed-authn/external-token',
body: {
externalAccessToken: mockExternalToken,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody?.id).toHaveLength(21)
expect(responseBody?.firstName).toBe(mockExternalTokenPayload.firstName)
expect(responseBody?.lastName).toBe(mockExternalTokenPayload.lastName)
expect(responseBody?.trackEvents).toBe(true)
expect(responseBody?.newsLetter).toBe(false)
expect(responseBody?.password).toBeUndefined()
expect(responseBody?.status).toBe('ACTIVE')
expect(responseBody?.verified).toBe(true)
expect(responseBody?.externalId).toBe(
mockExternalTokenPayload.externalUserId,
)
expect(responseBody?.platformId).toBe(mockPlatform.id)
expect(responseBody?.projectId).toHaveLength(21)
expect(responseBody?.token).toBeDefined()
})
it('Creates new project', async () => {
// arrange
const { mockPlatform } = await mockAndSaveBasicSetup()
const mockSigningKey = createMockSigningKey({
platformId: mockPlatform.id,
})
await databaseConnection()
.getRepository('signing_key')
.save(mockSigningKey)
const { mockExternalToken, mockExternalTokenPayload } =
generateMockExternalToken({
platformId: mockPlatform.id,
signingKeyId: mockSigningKey.id,
})
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/managed-authn/external-token',
body: {
externalAccessToken: mockExternalToken,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
const generatedProject = await databaseConnection()
.getRepository('project')
.findOneBy({
id: responseBody?.projectId,
})
expect(generatedProject?.displayName).toBe(
mockExternalTokenPayload.externalProjectId,
)
expect(generatedProject?.ownerId).toBe(mockPlatform.ownerId)
expect(generatedProject?.platformId).toBe(mockPlatform.id)
expect(generatedProject?.externalId).toBe(
mockExternalTokenPayload.externalProjectId,
)
})
it('Sync Pieces when exchanging external token', async () => {
// arrange
const { mockPlatform } = await mockAndSaveBasicSetup()
const mockPieceMetadata1 = createMockPieceMetadata({
name: '@ap/a',
version: '0.0.1',
pieceType: PieceType.OFFICIAL,
})
await databaseConnection()
.getRepository('piece_metadata')
.save(mockPieceMetadata1)
const mockTag = createMockTag({
id: apId(),
platformId: mockPlatform.id,
name: 'free',
})
await databaseConnection()
.getRepository('tag')
.save(mockTag)
const mockPieceTag = createMockPieceTag({
platformId: mockPlatform.id,
tagId: mockTag.id,
pieceName: '@ap/a',
})
await databaseConnection()
.getRepository('piece_tag')
.save(mockPieceTag)
const mockSigningKey = createMockSigningKey({
platformId: mockPlatform.id,
})
await databaseConnection()
.getRepository('signing_key')
.save(mockSigningKey)
const { mockExternalToken } = generateMockExternalToken({
platformId: mockPlatform.id,
signingKeyId: mockSigningKey.id,
pieces: {
filterType: PiecesFilterType.ALLOWED,
tags: ['free'],
},
})
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/managed-authn/external-token',
body: {
externalAccessToken: mockExternalToken,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
const generatedProject = await databaseConnection()
.getRepository('project_plan')
.findOneBy({ projectId: responseBody?.projectId })
expect(generatedProject?.piecesFilterType).toBe('ALLOWED')
expect(generatedProject?.pieces).toStrictEqual(['@ap/a'])
})
it('Adds new user as a member in new project', async () => {
// arrange
const { mockPlatform } = await mockAndSaveBasicSetup()
const mockSigningKey = createMockSigningKey({
platformId: mockPlatform.id,
})
await databaseConnection()
.getRepository('signing_key')
.save(mockSigningKey)
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.VIEWER }) as ProjectRole
const { mockExternalToken } = generateMockExternalToken({
platformId: mockPlatform.id,
signingKeyId: mockSigningKey.id,
projectRole: projectRole.name,
})
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/managed-authn/external-token',
body: {
externalAccessToken: mockExternalToken,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
const generatedProjectMember = await databaseConnection()
.getRepository('project_member')
.findOneBy({
projectId: responseBody?.projectId,
userId: responseBody?.id,
})
expect(generatedProjectMember?.projectId).toBe(responseBody?.projectId)
expect(generatedProjectMember?.userId).toBe(responseBody?.id)
expect(generatedProjectMember?.platformId).toBe(mockPlatform.id)
expect(generatedProjectMember?.projectRoleId).toBe(projectRole.id)
})
it('Adds new user to existing project', async () => {
// arrange
const { mockOwner, mockPlatform } = await mockAndSaveBasicSetup()
const mockSigningKey = createMockSigningKey({
platformId: mockPlatform.id,
})
await databaseConnection()
.getRepository('signing_key')
.save(mockSigningKey)
const mockExternalProjectId = apId()
const mockProject = createMockProject({
ownerId: mockOwner.id,
platformId: mockPlatform.id,
externalId: mockExternalProjectId,
})
await databaseConnection().getRepository('project').save(mockProject)
const { mockExternalToken } = generateMockExternalToken({
platformId: mockPlatform.id,
signingKeyId: mockSigningKey.id,
externalProjectId: mockExternalProjectId,
})
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/managed-authn/external-token',
body: {
externalAccessToken: mockExternalToken,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody?.projectId).toBe(mockProject.id)
})
it('Signs in existing users', async () => {
// arrange
const { mockOwner, mockPlatform } = await mockAndSaveBasicSetup()
const mockSigningKey = createMockSigningKey({
platformId: mockPlatform.id,
})
await databaseConnection()
.getRepository('signing_key')
.save(mockSigningKey)
const { mockExternalToken, mockExternalTokenPayload } = generateMockExternalToken({
platformId: mockPlatform.id,
signingKeyId: mockSigningKey.id,
})
const { mockUser } = await mockBasicUser({
user: {
externalId: mockExternalTokenPayload.externalUserId,
platformId: mockPlatform.id,
},
})
const mockProject = createMockProject({
ownerId: mockOwner.id,
platformId: mockPlatform.id,
externalId: mockExternalTokenPayload.externalProjectId,
})
await databaseConnection().getRepository('project').save(mockProject)
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/managed-authn/external-token',
body: {
externalAccessToken: mockExternalToken,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody?.projectId).toBe(mockProject.id)
expect(responseBody?.id).toBe(mockUser.id)
})
it('Fails if signing key is not found', async () => {
// arrange
await mockAndSaveBasicSetup()
const nonExistentSigningKeyId = apId()
const { mockExternalToken } = generateMockExternalToken({
signingKeyId: nonExistentSigningKeyId,
})
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/managed-authn/external-token',
body: {
externalAccessToken: mockExternalToken,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.UNAUTHORIZED)
expect(responseBody?.params?.message).toBe(
`signing key not found signingKeyId=${nonExistentSigningKeyId}`,
)
})
})
})

View File

@@ -0,0 +1,237 @@
import { UpsertOAuth2AppRequest } from '@activepieces/ee-shared'
import { PlatformRole, PrincipalType } from '@activepieces/shared'
import { faker } from '@faker-js/faker'
import { FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
import { generateMockToken } from '../../../helpers/auth'
import {
createMockOAuthApp,
mockAndSaveBasicSetup,
mockBasicUser,
} from '../../../helpers/mocks'
let app: FastifyInstance | null = null
const upsertRequest: UpsertOAuth2AppRequest = {
pieceName: faker.lorem.word(),
clientId: faker.lorem.word(),
clientSecret: faker.lorem.word(),
}
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('OAuth App API', () => {
describe('Upsert OAuth APP API', () => {
it('new OAuth App', async () => {
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockOwner.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
const response = await app?.inject({
method: 'POST',
url: '/v1/oauth-apps',
body: upsertRequest,
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody.id).toHaveLength(21)
expect(responseBody.platformId).toBe(mockPlatform.id)
expect(responseBody.pieceName).toBe(upsertRequest.pieceName)
expect(responseBody.clientId).toBe(upsertRequest.clientId)
expect(responseBody.clientSecret).toBeUndefined()
})
it('Fails if user is not platform owner', async () => {
// arrange
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockUser.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
const response = await app?.inject({
method: 'POST',
url: '/v1/oauth-apps',
body: upsertRequest,
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
})
describe('Delete OAuth App', () => {
it('Forbid by Non Owner', async () => {
// arrange
const { mockOwner: mockUserTwo, mockPlatform: mockPlatformTwo, mockProject: mockProjectTwo } = await mockAndSaveBasicSetup()
const mockOAuthApp = await createMockOAuthApp({
platformId: mockPlatformTwo.id,
})
await databaseConnection().getRepository('user').update(mockUserTwo.id, {
platformRole: PlatformRole.MEMBER,
})
await databaseConnection().getRepository('oauth_app').save(mockOAuthApp)
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockUserTwo.id,
projectId: mockProjectTwo.id,
platform: { id: mockPlatformTwo.id },
})
// act
const response = await app?.inject({
method: 'DELETE',
url: `/v1/oauth-apps/${mockOAuthApp.id}`,
headers: {
authorization: `Bearer ${testToken}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
it('By Id', async () => {
// arrange
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const mockOAuthApp = await createMockOAuthApp({
platformId: mockPlatform.id,
})
await databaseConnection().getRepository('oauth_app').save(mockOAuthApp)
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockOwner.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
// act
const response = await app?.inject({
method: 'DELETE',
url: `/v1/oauth-apps/${mockOAuthApp.id}`,
headers: {
authorization: `Bearer ${testToken}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.OK)
})
})
describe('List OAuth Apps endpoint', () => {
it('should list OAuth Apps by platform owner', async () => {
// arrange
const { mockOwner: mockUserOne, mockPlatform: mockPlatformOne, mockProject: mockProjectOne } = await mockAndSaveBasicSetup()
const mockOAuthAppsOne = await createMockOAuthApp({
platformId: mockPlatformOne.id,
})
await databaseConnection()
.getRepository('oauth_app')
.save([mockOAuthAppsOne])
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockUserOne.id,
projectId: mockProjectOne.id,
platform: { id: mockPlatformOne.id },
})
// act
const response = await app?.inject({
method: 'GET',
url: '/v1/oauth-apps',
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody.data).toHaveLength(1)
expect(responseBody.data[0].id).toBe(mockOAuthAppsOne.id)
expect(responseBody.data[0].clientSecret).toBeUndefined()
})
it('should list OAuth Apps by platform member', async () => {
// arrange
const { mockPlatform: mockPlatformOne, mockProject: mockProjectOne } = await mockAndSaveBasicSetup()
const { mockOwner: mockUserTwo, mockPlatform: mockPlatformTwo } = await mockAndSaveBasicSetup()
const mockOAuthAppsOne = await createMockOAuthApp({
platformId: mockPlatformOne.id,
})
const mockOAuthAppsTwo = await createMockOAuthApp({
platformId: mockPlatformTwo.id,
})
await databaseConnection()
.getRepository('oauth_app')
.save([mockOAuthAppsOne, mockOAuthAppsTwo])
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockUserTwo.id,
projectId: mockProjectOne.id,
platform: { id: mockPlatformOne.id },
})
// act
const response = await app?.inject({
method: 'GET',
url: '/v1/oauth-apps',
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody.data).toHaveLength(1)
expect(responseBody.data[0].id).toBe(mockOAuthAppsOne.id)
expect(responseBody.data[0].clientSecret).toBeUndefined()
})
})
})

View File

@@ -0,0 +1,122 @@
import { OtpType } from '@activepieces/ee-shared'
import { FastifyBaseLogger, FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import * as emailServiceFile from '../../../../src/app/ee/helper/email/email-service'
import { setupServer } from '../../../../src/app/server'
import { mockAndSaveBasicSetup } from '../../../helpers/mocks'
let app: FastifyInstance | null = null
let sendOtpSpy: jest.Mock
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
beforeEach(() => {
sendOtpSpy = jest.fn()
jest.spyOn(emailServiceFile, 'emailService').mockImplementation((_log: FastifyBaseLogger) => ({
sendOtp: sendOtpSpy,
sendInvitation: jest.fn(),
sendIssueCreatedNotification: jest.fn(),
sendQuotaAlert: jest.fn(),
sendReminderJobHandler: jest.fn(),
sendExceedFailureThresholdAlert: jest.fn(),
}))
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('OTP API', () => {
describe('Create and Send Endpoint', () => {
it('Generates new OTP', async () => {
const { mockUserIdentity } = await mockAndSaveBasicSetup()
const mockCreateOtpRequest = {
email: mockUserIdentity.email,
type: OtpType.EMAIL_VERIFICATION,
}
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/otp',
body: mockCreateOtpRequest,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.NO_CONTENT)
})
it('Sends OTP to user', async () => {
const { mockUserIdentity } = await mockAndSaveBasicSetup()
await databaseConnection().getRepository('user_identity').update(mockUserIdentity.id, {
verified: false,
})
const mockCreateOtpRequest = {
email: mockUserIdentity.email,
type: OtpType.EMAIL_VERIFICATION,
}
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/otp',
body: mockCreateOtpRequest,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.NO_CONTENT)
expect(sendOtpSpy).toHaveBeenCalledTimes(1)
expect(sendOtpSpy).toHaveBeenCalledWith({
otp: expect.stringMatching(/^([0-9A-F]|-){36}$/i),
platformId: null,
type: OtpType.EMAIL_VERIFICATION,
userIdentity: expect.objectContaining({
email: mockUserIdentity.email,
}),
})
})
it('OTP is unique per user per OTP type', async () => {
const { mockUserIdentity } = await mockAndSaveBasicSetup()
const mockCreateOtpRequest = {
email: mockUserIdentity.email,
type: OtpType.EMAIL_VERIFICATION,
}
// act
const response1 = await app?.inject({
method: 'POST',
url: '/v1/otp',
body: mockCreateOtpRequest,
})
const response2 = await app?.inject({
method: 'POST',
url: '/v1/otp',
body: mockCreateOtpRequest,
})
// assert
expect(response1?.statusCode).toBe(StatusCodes.NO_CONTENT)
expect(response2?.statusCode).toBe(StatusCodes.NO_CONTENT)
const otpCount = await databaseConnection().getRepository('otp').countBy({
identityId: mockUserIdentity.id,
type: mockCreateOtpRequest.type,
})
expect(otpCount).toBe(1)
})
})
})

View File

@@ -0,0 +1,653 @@
import {
apId,
FilteredPieceBehavior,
PiecesFilterType,
PieceType,
PlatformRole,
PrincipalType,
} from '@activepieces/shared'
import { FastifyBaseLogger, FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { localPieceCache } from '../../../../src/app/pieces/metadata/local-piece-cache'
import { setupServer } from '../../../../src/app/server'
import { generateMockToken } from '../../../helpers/auth'
import {
createMockPieceMetadata,
createMockPlan,
createMockProject,
mockAndSaveBasicSetup,
mockBasicUser,
} from '../../../helpers/mocks'
let app: FastifyInstance | null = null
let mockLog: FastifyBaseLogger
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
mockLog = app!.log!
})
beforeEach(async () => {
await databaseConnection().getRepository('piece_metadata').createQueryBuilder().delete().execute()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('Piece Metadata API', () => {
describe('List Piece Versions endpoint', () => {
it('Should return versions in sorted order for a piece', async () => {
// arrange
const mockPieceMetadata1 = createMockPieceMetadata({
name: '@ap/a',
version: '0.0.1',
pieceType: PieceType.OFFICIAL,
})
await databaseConnection()
.getRepository('piece_metadata')
.save(mockPieceMetadata1)
const mockPieceMetadata2 = createMockPieceMetadata({
name: '@ap/a',
version: '0.0.2',
pieceType: PieceType.OFFICIAL,
})
await databaseConnection()
.getRepository('piece_metadata')
.save(mockPieceMetadata2)
await localPieceCache(mockLog).setup()
const testToken = await generateMockToken({
type: PrincipalType.UNKNOWN,
id: apId(),
})
// act
const response = await app?.inject({
method: 'GET',
url: '/v1/pieces/versions?release=1.1.1&name=@ap/a',
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
const keys = Object.keys(responseBody)
expect(keys).toHaveLength(2)
expect(keys[0]).toBe('0.0.1')
expect(keys[1]).toBe('0.0.2')
})
})
describe('Get Piece metadata', () => {
it('Should return metadata when authenticated', async () => {
// arrange
const mockPieceMetadata = createMockPieceMetadata({
name: '@activepieces/a',
pieceType: PieceType.OFFICIAL,
})
await databaseConnection()
.getRepository('piece_metadata')
.save(mockPieceMetadata)
await localPieceCache(mockLog).setup()
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup({
platform: {
filteredPieceBehavior: FilteredPieceBehavior.BLOCKED,
filteredPieceNames: [],
},
})
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockOwner.id,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
// act
const response = await app?.inject({
method: 'GET',
url: '/v1/pieces/@activepieces/a',
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody.id).toBe(mockPieceMetadata.id)
})
it('Should return metadata when not authenticated', async () => {
// arrange
const mockPieceMetadata = createMockPieceMetadata({
name: '@activepieces/a',
pieceType: PieceType.OFFICIAL,
displayName: 'a',
})
await databaseConnection()
.getRepository('piece_metadata')
.save(mockPieceMetadata)
await localPieceCache(mockLog).setup()
const testToken = await generateMockToken({
type: PrincipalType.UNKNOWN,
id: apId(),
})
// act
const response = await app?.inject({
method: 'GET',
url: '/v1/pieces/@activepieces/a',
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
// Expectations for each attribute
expect(responseBody.actions).toEqual(mockPieceMetadata.actions)
expect(responseBody.triggers).toEqual(mockPieceMetadata.triggers)
expect(responseBody.archiveId).toBe(mockPieceMetadata.archiveId)
expect(responseBody.auth).toEqual(mockPieceMetadata.auth)
expect(responseBody.description).toBe(mockPieceMetadata.description)
expect(responseBody.directoryPath).toBe(mockPieceMetadata.directoryPath)
expect(responseBody.displayName).toBe(mockPieceMetadata.displayName)
expect(responseBody.id).toBe(mockPieceMetadata.id)
expect(responseBody.logoUrl).toBe(mockPieceMetadata.logoUrl)
expect(responseBody.maximumSupportedRelease).toBe(
mockPieceMetadata.maximumSupportedRelease,
)
expect(responseBody.minimumSupportedRelease).toBe(
mockPieceMetadata.minimumSupportedRelease,
)
expect(responseBody.packageType).toBe(mockPieceMetadata.packageType)
expect(responseBody.pieceType).toBe(mockPieceMetadata.pieceType)
expect(responseBody.platformId).toBe(mockPieceMetadata.platformId)
expect(responseBody.projectId).toBe(mockPieceMetadata.projectId)
expect(responseBody.version).toBe(mockPieceMetadata.version)
})
})
describe('List Piece Metadata endpoint', () => {
it('Should list platform pieces', async () => {
const { mockOwner, mockPlatform } = await mockAndSaveBasicSetup({
platform: {
filteredPieceBehavior: FilteredPieceBehavior.BLOCKED,
filteredPieceNames: [],
},
})
const { mockPlatform: mockPlatform2 } = await mockAndSaveBasicSetup({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const mockProject = await createProjectAndPlan({
platformId: mockPlatform.id,
ownerId: mockOwner.id,
})
// arrange
const mockPieceMetadataA = createMockPieceMetadata({
name: 'a',
pieceType: PieceType.CUSTOM,
platformId: mockPlatform.id,
displayName: 'a',
})
const mockPieceMetadataB = createMockPieceMetadata({
name: 'b',
pieceType: PieceType.OFFICIAL,
displayName: 'b',
})
const mockPieceMetadataC = createMockPieceMetadata({
name: 'c',
pieceType: PieceType.CUSTOM,
platformId: mockPlatform2.id,
displayName: 'c',
})
const mockPieceMetadataD = createMockPieceMetadata({
name: 'd',
pieceType: PieceType.CUSTOM,
platformId: mockPlatform.id,
displayName: 'd',
})
await databaseConnection()
.getRepository('piece_metadata')
.save([
mockPieceMetadataA,
mockPieceMetadataB,
mockPieceMetadataC,
mockPieceMetadataD,
])
await localPieceCache(mockLog).setup()
const testToken = await generateMockToken({
type: PrincipalType.USER,
projectId: mockProject.id,
id: mockOwner.id,
platform: {
id: mockPlatform.id,
},
})
// act
const response = await app?.inject({
method: 'GET',
url: '/v1/pieces?release=1.1.1',
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody).toHaveLength(3)
expect(responseBody?.[0].id).toBe(mockPieceMetadataA.id)
expect(responseBody?.[1].id).toBe(mockPieceMetadataB.id)
expect(responseBody?.[2].id).toBe(mockPieceMetadataD.id)
})
it('Should list correct version by piece name', async () => {
// arrange
const mockPieceMetadataA = createMockPieceMetadata({
name: 'a',
pieceType: PieceType.OFFICIAL,
displayName: 'a',
version: '0.0.1',
})
const mockPieceMetadataB = createMockPieceMetadata({
name: 'a',
pieceType: PieceType.OFFICIAL,
displayName: 'a',
version: '0.0.2',
})
const mockPieceMetadataC = createMockPieceMetadata({
name: 'a',
pieceType: PieceType.OFFICIAL,
displayName: 'a',
version: '0.1.0',
})
const mockPieceMetadataD = createMockPieceMetadata({
name: 'a',
pieceType: PieceType.OFFICIAL,
displayName: 'a',
version: '0.1.1',
})
await databaseConnection()
.getRepository('piece_metadata')
.save([mockPieceMetadataA, mockPieceMetadataB, mockPieceMetadataC, mockPieceMetadataD])
const testToken = await generateMockToken({
type: PrincipalType.UNKNOWN,
id: apId(),
})
await localPieceCache(mockLog).setup()
// act
const exactVersionResponse = await app?.inject({
method: 'GET',
url: '/v1/pieces/a?version=0.0.1',
headers: {
authorization: `Bearer ${testToken}`,
},
})
const exactVersionResponseBody = exactVersionResponse?.json()
expect(exactVersionResponse?.statusCode).toBe(StatusCodes.OK)
expect(exactVersionResponseBody?.id).toBe(mockPieceMetadataA.id)
const telda2VersionResponse = await app?.inject({
method: 'GET',
url: '/v1/pieces/a?version=~0.0.2',
headers: {
authorization: `Bearer ${testToken}`,
},
})
const teldaVersion2ResponseBody = telda2VersionResponse?.json()
expect(telda2VersionResponse?.statusCode).toBe(StatusCodes.OK)
expect(teldaVersion2ResponseBody?.id).toBe(mockPieceMetadataB.id)
const teldaVersionResponse = await app?.inject({
method: 'GET',
url: '/v1/pieces/a?version=~0.0.1',
headers: {
authorization: `Bearer ${testToken}`,
},
})
const teldaVersionResponseBody = teldaVersionResponse?.json()
expect(teldaVersionResponse?.statusCode).toBe(StatusCodes.OK)
expect(teldaVersionResponseBody?.id).toBe(mockPieceMetadataB.id)
const notFoundVersionResponse = await app?.inject({
method: 'GET',
url: '/v1/pieces/a?version=~0.1.2',
headers: {
authorization: `Bearer ${testToken}`,
},
})
expect(notFoundVersionResponse?.statusCode).toBe(StatusCodes.NOT_FOUND)
})
it('Should list latest version by piece name', async () => {
// arrange
const mockPieceMetadataA = createMockPieceMetadata({
name: 'a',
pieceType: PieceType.OFFICIAL,
displayName: 'a',
version: '0.31.0',
})
const mockPieceMetadataB = createMockPieceMetadata({
name: 'a',
pieceType: PieceType.OFFICIAL,
displayName: 'a',
version: '1.0.0',
})
await databaseConnection()
.getRepository('piece_metadata')
.save([mockPieceMetadataA, mockPieceMetadataB])
await localPieceCache(mockLog).setup()
const testToken = await generateMockToken({
type: PrincipalType.UNKNOWN,
id: apId(),
})
// act
const response = await app?.inject({
method: 'GET',
url: '/v1/pieces?release=1.1.1',
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody).toHaveLength(1)
expect(responseBody?.[0].id).toBe(mockPieceMetadataB.id)
})
it('Sorts by piece name', async () => {
// arrange
const mockPieceMetadataA = createMockPieceMetadata({
name: 'a',
pieceType: PieceType.OFFICIAL,
displayName: 'a',
})
const mockPieceMetadataB = createMockPieceMetadata({
name: 'b',
pieceType: PieceType.OFFICIAL,
displayName: 'b',
})
await databaseConnection()
.getRepository('piece_metadata')
.save([mockPieceMetadataA, mockPieceMetadataB])
await localPieceCache(mockLog).setup()
const testToken = await generateMockToken({
type: PrincipalType.UNKNOWN,
id: apId(),
})
// act
const response = await app?.inject({
method: 'GET',
url: '/v1/pieces?release=1.1.1',
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody).toHaveLength(2)
expect(responseBody?.[0].id).toBe(mockPieceMetadataA.id)
expect(responseBody?.[1].id).toBe(mockPieceMetadataB.id)
})
it('Allows filtered pieces if project filter is set to "ALLOWED"', async () => {
// arrange
const { mockPlatform } = await mockAndSaveBasicSetup({
platform: {
filteredPieceBehavior: FilteredPieceBehavior.BLOCKED,
filteredPieceNames: [],
},
})
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const mockProject = await createProjectAndPlan({
ownerId: mockUser.id,
platformId: mockPlatform.id,
piecesFilterType: PiecesFilterType.ALLOWED,
pieces: ['a'],
})
const mockPieceMetadataA = createMockPieceMetadata({
name: 'a',
pieceType: PieceType.OFFICIAL,
displayName: 'a',
})
const mockPieceMetadataB = createMockPieceMetadata({
name: 'b',
pieceType: PieceType.OFFICIAL,
displayName: 'b',
})
await databaseConnection()
.getRepository('piece_metadata')
.save([mockPieceMetadataA, mockPieceMetadataB])
await localPieceCache(mockLog).setup()
const testToken = await generateMockToken({
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
id: mockUser.id,
})
// act
const response = await app?.inject({
method: 'GET',
url: '/v1/pieces?release=1.1.1',
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody).toHaveLength(1)
expect(responseBody?.[0].id).toBe(mockPieceMetadataA.id)
})
it('Allows filtered pieces if platform filter is set to "ALLOWED"', async () => {
// arrange
const { mockPlatform } = await mockAndSaveBasicSetup({
platform: {
filteredPieceNames: ['a'],
filteredPieceBehavior: FilteredPieceBehavior.ALLOWED,
},
})
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const mockProject = await createProjectAndPlan({
ownerId: mockUser.id,
platformId: mockPlatform.id,
})
const mockPieceMetadataA = createMockPieceMetadata({
name: 'a',
pieceType: PieceType.OFFICIAL,
displayName: 'a',
})
const mockPieceMetadataB = createMockPieceMetadata({
name: 'b',
pieceType: PieceType.OFFICIAL,
displayName: 'b',
})
await databaseConnection()
.getRepository('piece_metadata')
.save([mockPieceMetadataA, mockPieceMetadataB])
await localPieceCache(mockLog).setup()
const testToken = await generateMockToken({
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
id: mockUser.id,
})
// act
const response = await app?.inject({
method: 'GET',
url: '/v1/pieces?release=1.1.1',
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody).toHaveLength(1)
expect(responseBody?.[0].id).toBe(mockPieceMetadataA.id)
})
it('Blocks filtered pieces if platform filter is set to "BLOCKED"', async () => {
// arrange
const { mockPlatform } = await mockAndSaveBasicSetup({
platform: {
filteredPieceNames: ['a'],
filteredPieceBehavior: FilteredPieceBehavior.BLOCKED,
},
})
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const mockProject = await createProjectAndPlan({
ownerId: mockUser.id,
platformId: mockPlatform.id,
})
const mockPieceMetadataA = createMockPieceMetadata({
name: 'a',
pieceType: PieceType.OFFICIAL,
displayName: 'a',
})
const mockPieceMetadataB = createMockPieceMetadata({
name: 'b',
pieceType: PieceType.OFFICIAL,
displayName: 'b',
})
await databaseConnection()
.getRepository('piece_metadata')
.save([mockPieceMetadataA, mockPieceMetadataB])
await localPieceCache(mockLog).setup()
const testToken = await generateMockToken({
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
id: mockUser.id,
})
// act
const response = await app?.inject({
method: 'GET',
url: '/v1/pieces?release=1.1.1',
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody).toHaveLength(1)
expect(responseBody?.[0].id).toBe(mockPieceMetadataB.id)
})
})
})
async function createProjectAndPlan({
platformId,
ownerId,
piecesFilterType,
pieces,
}: {
platformId: string
ownerId: string
piecesFilterType?: PiecesFilterType
pieces?: string[]
}) {
const project = createMockProject({
platformId,
ownerId,
})
await databaseConnection().getRepository('project').save([project])
const projectPlan = createMockPlan({
projectId: project.id,
piecesFilterType,
pieces,
})
await databaseConnection().getRepository('project_plan').save([projectPlan])
return project
}

View File

@@ -0,0 +1,440 @@
import { apId, FilteredPieceBehavior,
PlanName,
PlatformRole,
PrincipalType,
UpdatePlatformRequestBody,
} from '@activepieces/shared'
import { faker } from '@faker-js/faker'
import { FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
import { generateMockToken } from '../../../helpers/auth'
import { checkIfSolutionExistsInDb, createMockSolutionAndSave, createMockUser, mockAndSaveBasicSetup, mockBasicUser } from '../../../helpers/mocks'
let app: FastifyInstance | null = null
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('Platform API', () => {
describe('update platform endpoint', () => {
it('patches a platform by id', async () => {
// arrange
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup({
plan: {
embeddingEnabled: false,
},
platform: {
},
})
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockOwner.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
const requestBody: UpdatePlatformRequestBody = {
name: 'updated name',
primaryColor: 'updated primary color',
filteredPieceNames: ['updated filtered piece names'],
filteredPieceBehavior: FilteredPieceBehavior.ALLOWED,
enforceAllowedAuthDomains: true,
allowedAuthDomains: ['yahoo.com'],
cloudAuthEnabled: false,
emailAuthEnabled: false,
}
// act
const response = await app?.inject({
method: 'POST',
url: `/v1/platforms/${mockPlatform.id}`,
headers: {
authorization: `Bearer ${testToken}`,
},
body: requestBody,
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody.id).toBe(mockPlatform.id)
expect(responseBody.created).toBeDefined()
expect(responseBody.updated).toBeDefined()
expect(responseBody.enforceAllowedAuthDomains).toBe(
requestBody.enforceAllowedAuthDomains,
)
expect(responseBody.allowedAuthDomains).toEqual(
requestBody.allowedAuthDomains,
)
expect(responseBody.ownerId).toBe(mockOwner.id)
expect(responseBody.emailAuthEnabled).toBe(requestBody.emailAuthEnabled)
expect(responseBody.name).toBe('updated name')
expect(responseBody.primaryColor).toBe('updated primary color')
expect(responseBody.filteredPieceNames).toStrictEqual([
'updated filtered piece names',
])
expect(responseBody.filteredPieceBehavior).toBe('ALLOWED')
expect(responseBody.emailAuthEnabled).toBe(false)
expect(responseBody.federatedAuthProviders).toStrictEqual({})
expect(responseBody.cloudAuthEnabled).toBe(false)
}),
it('updates the platform logo icons', async () => {
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup({
plan: {
embeddingEnabled: false,
},
platform: {
},
})
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockOwner.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
const formData = new FormData()
formData.append('logoIcon', new Blob([faker.image.urlPlaceholder()], { type: 'image/png' }))
formData.append('fullLogo', new Blob([faker.image.urlPlaceholder()], { type: 'image/png' }))
formData.append('favIcon', new Blob([faker.image.urlPlaceholder()], { type: 'image/png' }))
formData.append('name', 'updated name')
// act
const response = await app?.inject({
method: 'POST',
url: `/v1/platforms/${mockPlatform.id}`,
headers: {
authorization: `Bearer ${testToken}`,
},
body: formData,
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody.id).toBe(mockPlatform.id)
expect(responseBody.created).toBeDefined()
expect(responseBody.updated).toBeDefined()
expect(responseBody.name).toBe('updated name')
const baseUrl = 'http://localhost:4200/api/v1/platforms/assets'
expect(responseBody.logoIconUrl.startsWith(baseUrl)).toBeTruthy()
expect(responseBody.fullLogoUrl.startsWith(baseUrl)).toBeTruthy()
expect(responseBody.favIconUrl.startsWith(baseUrl)).toBeTruthy()
}),
it('fails if user is not owner', async () => {
// arrange
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockUser.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
// act
const response = await app?.inject({
method: 'POST',
url: `/v1/platforms/${mockPlatform.id}`,
headers: {
authorization: `Bearer ${testToken}`,
},
body: {
primaryColor: '#000000',
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
})
describe('get platform endpoint', () => {
it('Always Returns non-sensitive information for platform', async () => {
// arrange
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup({
platform: {
federatedAuthProviders: {
google: {
clientId: faker.internet.password(),
clientSecret: faker.internet.password(),
},
saml: {
idpCertificate: faker.internet.password(),
idpMetadata: faker.internet.password(),
},
},
},
})
const mockToken = await generateMockToken({
type: PrincipalType.USER,
id: mockOwner.id,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
// act
const response = await app?.inject({
method: 'GET',
url: `/v1/platforms/${mockPlatform.id}`,
headers: {
authorization: `Bearer ${mockToken}`,
},
})
const responseBody = response?.json()
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(Object.keys(responseBody).length).toBe(19)
expect(responseBody.id).toBe(mockPlatform.id)
expect(responseBody.ownerId).toBe(mockOwner.id)
expect(responseBody.name).toBe(mockPlatform.name)
expect(responseBody.federatedAuthProviders.google).toStrictEqual({
clientId: mockPlatform.federatedAuthProviders?.google?.clientId,
})
expect(responseBody.federatedAuthProviders.saml).toStrictEqual({})
expect(responseBody.primaryColor).toBe(mockPlatform.primaryColor)
expect(responseBody.logoIconUrl).toBe(mockPlatform.logoIconUrl)
expect(responseBody.fullLogoUrl).toBe(mockPlatform.fullLogoUrl)
expect(responseBody.favIconUrl).toBe(mockPlatform.favIconUrl)
})
it('Fails if user is not a platform member', async () => {
const { mockOwner: mockOwner1, mockPlatform: mockPlatform1, mockProject: mockProject1 } = await mockAndSaveBasicSetup()
const { mockPlatform: mockPlatform2 } = await mockAndSaveBasicSetup()
const mockToken = await generateMockToken({
type: PrincipalType.USER,
id: mockOwner1.id,
projectId: mockProject1.id,
platform: {
id: mockPlatform1.id,
},
})
// act
const response = await app?.inject({
method: 'GET',
url: `/v1/platforms/${mockPlatform2.id}`,
headers: {
authorization: `Bearer ${mockToken}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
}),
describe('delete platform endpoint', () => {
it('deletes a platform by id', async () => {
// arrange
const firstAccount = await mockAndSaveBasicSetup( {
plan: {
plan: PlanName.STANDARD,
},
})
const secondAccount = await mockAndSaveBasicSetup(
{
plan: {
plan: PlanName.STANDARD,
},
},
)
const ownerSolution = await createMockSolutionAndSave({ projectId: firstAccount.mockProject.id, platformId: firstAccount.mockPlatform.id, userId: firstAccount.mockOwner.id })
const secondSolution = await createMockSolutionAndSave({ projectId: secondAccount.mockProject.id, platformId: secondAccount.mockPlatform.id, userId: secondAccount.mockOwner.id })
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: firstAccount.mockOwner.id,
projectId: firstAccount.mockProject.id,
platform: { id: firstAccount.mockPlatform.id },
})
// act
const response = await app?.inject({
method: 'DELETE',
url: `/v1/platforms/${firstAccount.mockPlatform.id}`,
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.NO_CONTENT)
const secondSolutionExists = await checkIfSolutionExistsInDb(secondSolution)
expect(secondSolutionExists).toBe(true)
const ownerSolutionExists = await checkIfSolutionExistsInDb(ownerSolution)
expect(ownerSolutionExists).toBe(false)
}),
it('fails if platform is not eligible for deletion', async () => {
// arrange
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup( {
plan: {
plan: PlanName.ENTERPRISE,
},
})
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockOwner.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
// act
const response = await app?.inject({
method: 'DELETE',
url: `/v1/platforms/${mockPlatform.id}`,
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.UNPROCESSABLE_ENTITY)
}),
it('fails if user is not owner', async () => {
// arrange
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup( {
plan: {
plan: PlanName.STANDARD,
},
})
const secondAccount = await mockAndSaveBasicSetup(
{
plan: {
plan: PlanName.STANDARD,
},
},
)
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockOwner.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
// act
const response = await app?.inject({
method: 'DELETE',
url: `/v1/platforms/${secondAccount.mockPlatform.id}`,
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
}),
it('doesn\'t delete user identity if it has other users', async () => {
// arrange
const firstAccount = await mockAndSaveBasicSetup( {
plan: {
plan: PlanName.STANDARD,
},
})
const secondPlatform = await mockAndSaveBasicSetup( {
plan: {
plan: PlanName.STANDARD,
},
})
const secondUser = createMockUser({
platformId: secondPlatform.mockPlatform.id,
platformRole: PlatformRole.ADMIN,
identityId: firstAccount.mockUserIdentity.id,
})
await databaseConnection().getRepository('user').save(secondUser)
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: firstAccount.mockOwner.id,
projectId: firstAccount.mockProject.id,
platform: { id: firstAccount.mockPlatform.id },
})
// act
const response = await app?.inject({
method: 'DELETE',
url: `/v1/platforms/${firstAccount.mockPlatform.id}`,
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.NO_CONTENT)
const userIdentityExists = await databaseConnection().getRepository('user_identity').findOneBy({ id: firstAccount.mockUserIdentity.id })
expect(userIdentityExists).not.toBeNull()
})
})
describe('get platform endpoint', () => {
it('fails if user is not part of the platform', async () => {
// arrange
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockOwner.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
// act
const response = await app?.inject({
method: 'GET',
url: `/v1/platforms/${apId()}`,
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
}),
it('succeeds if user is part of the platform and is not admin', async () => {
// arrange
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
await databaseConnection().getRepository('user').save(mockUser)
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockUser.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
// act
const response = await app?.inject({
method: 'GET',
url: `/v1/platforms/${mockPlatform.id}`,
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
})
})

View File

@@ -0,0 +1,484 @@
import {
ApiKeyResponseWithValue,
UpdateProjectMemberRoleRequestBody,
} from '@activepieces/ee-shared'
import { DefaultProjectRole, Permission, Platform, PlatformRole, PrincipalType, Project, ProjectRole, RoleType, User } from '@activepieces/shared'
import { FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
import { generateMockToken } from '../../../helpers/auth'
import {
createMockProject,
createMockProjectMember,
createMockProjectRole,
mockAndSaveBasicSetup,
mockAndSaveBasicSetupWithApiKey,
mockBasicUser,
} from '../../../helpers/mocks'
let app: FastifyInstance | null = null
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('Project Member API', () => {
describe('Update project member role', () => {
it('should update a project role for a member', async () => {
const { mockOwner: mockUserOne, mockPlatform: mockPlatformOne, mockProject: mockProjectOne } = await mockAndSaveBasicSetup({
platform: {
},
plan: {
projectRolesEnabled: true,
auditLogEnabled: false,
},
})
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockUserOne.id,
projectId: mockProjectOne.id,
platform: { id: mockPlatformOne.id },
})
const projectRole = createMockProjectRole({ platformId: mockPlatformOne.id, type: RoleType.CUSTOM, permissions: [Permission.WRITE_PROJECT_MEMBER] })
await databaseConnection().getRepository('project_role').save(projectRole)
const mockProjectMemberOne = createMockProjectMember({ platformId: mockPlatformOne.id, projectId: mockProjectOne.id, projectRoleId: projectRole.id, userId: mockUserOne.id })
await databaseConnection().getRepository('project_member').save(mockProjectMemberOne)
const request: UpdateProjectMemberRoleRequestBody = {
role: 'VIEWER',
}
const response = await app?.inject({
method: 'POST',
url: `/v1/project-members/${mockProjectMemberOne.id}`,
body: request,
headers: {
authorization: `Bearer ${testToken}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.OK)
})
it('should fail to update project role when user does not have permission', async () => {
const { mockPlatform: mockPlatformOne, mockProject: mockProjectOne } = await mockAndSaveBasicSetup({
plan: {
projectRolesEnabled: true,
auditLogEnabled: false,
},
})
// Create a user who is not in the project
const { mockUser: viewerUser } = await mockBasicUser({
user: {
platformId: mockPlatformOne.id,
platformRole: PlatformRole.MEMBER,
},
})
const mockProjectTwo = createMockProject({
platformId: mockPlatformOne.id,
ownerId: viewerUser.id,
})
await databaseConnection().getRepository('project').save(mockProjectTwo)
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: viewerUser.id,
projectId: mockProjectTwo.id,
platform: { id: mockPlatformOne.id },
})
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({
name: DefaultProjectRole.VIEWER,
}) as ProjectRole
// Create a project member to try to modify
const mockProjectMember = createMockProjectMember({
platformId: mockPlatformOne.id,
projectId: mockProjectOne.id,
projectRoleId: projectRole.id,
userId: viewerUser.id,
})
await databaseConnection().getRepository('project_member').save(mockProjectMember)
const request: UpdateProjectMemberRoleRequestBody = {
role: DefaultProjectRole.ADMIN,
}
const response = await app?.inject({
method: 'POST',
url: `/v1/project-members/${mockProjectMember.id}`,
body: request,
headers: {
authorization: `Bearer ${testToken}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.NOT_FOUND)
})
it('should fail to update project role when user is admin of another project', async () => {
// Create first project with its platform
const { mockProject: projectOne, mockPlatform } = await mockAndSaveBasicSetup({
plan: {
projectRolesEnabled: true,
auditLogEnabled: false,
},
})
// Create second project admin
const { mockUser: adminOfProjectTwo } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const projectTwo = createMockProject({
ownerId: adminOfProjectTwo.id,
platformId: mockPlatform.id,
})
await databaseConnection().getRepository('project').save(projectTwo)
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: adminOfProjectTwo.id,
projectId: projectTwo.id,
platform: { id: mockPlatform.id },
})
// Create member in first project to try to modify
const { mockUser: memberToModify } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const viewerRole = await databaseConnection().getRepository('project_role').findOneByOrFail({
name: DefaultProjectRole.VIEWER,
}) as ProjectRole
const projectMember = createMockProjectMember({
platformId: mockPlatform.id,
projectId: projectOne.id,
projectRoleId: viewerRole.id,
userId: memberToModify.id,
})
await databaseConnection().getRepository('project_member').save(projectMember)
const request: UpdateProjectMemberRoleRequestBody = {
role: DefaultProjectRole.ADMIN,
}
const response = await app?.inject({
method: 'POST',
url: `/v1/project-members/${projectMember.id}`,
body: request,
headers: {
authorization: `Bearer ${testToken}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.NOT_FOUND)
})
})
describe('List project members Endpoint', () => {
describe('List project members from api', () => {
it('should return project members', async () => {
const { mockApiKey, mockProject, mockMember, mockPlatform } = await createBasicEnvironment()
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.VIEWER }) as ProjectRole
const mockProjectMember = createMockProjectMember({
projectId: mockProject.id,
userId: mockMember.id,
projectRoleId: projectRole.id,
platformId: mockPlatform.id,
})
await databaseConnection()
.getRepository('project_member')
.save(mockProjectMember)
// act
const response = await app?.inject({
method: 'GET',
url: `/v1/project-members?projectId=${mockProject.id}`,
headers: {
authorization: `Bearer ${mockApiKey.value}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.OK)
const responseBody = response?.json()
expect(responseBody.data).toHaveLength(1)
expect(responseBody.data[0].id).toBe(mockProjectMember.id)
})
it('Lists project members for non owner project', async () => {
const { mockApiKey, mockMember } = await createBasicEnvironment()
const { mockProject: mockProject2 } = await mockAndSaveBasicSetup({
plan: {
projectRolesEnabled: true,
auditLogEnabled: false,
},
})
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.VIEWER }) as ProjectRole
const mockProjectMember = createMockProjectMember({
projectId: mockProject2.id,
userId: mockMember.id,
projectRoleId: projectRole.id,
})
await databaseConnection()
.getRepository('project_member')
.save(mockProjectMember)
// act
const response = await app?.inject({
method: 'GET',
url: `/v1/project-members?projectId=${mockProject2.id}`,
headers: {
authorization: `Bearer ${mockApiKey.value}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
})
describe('List project members by user', () => {
it.each([
DefaultProjectRole.ADMIN,
DefaultProjectRole.EDITOR,
DefaultProjectRole.VIEWER,
DefaultProjectRole.OPERATOR,
])('Succeeds if user role is %s', async (testRole) => {
// arrange
const { mockPlatform, mockProject, mockMember } = await createBasicEnvironment()
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: testRole }) as ProjectRole
const mockProjectMember = createMockProjectMember({
userId: mockMember.id,
platformId: mockPlatform.id,
projectId: mockProject.id,
projectRoleId: projectRole.id,
})
await databaseConnection().getRepository('project_member').save([mockProjectMember])
const mockToken = await generateMockToken({
id: mockMember.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
// act
const response = await app?.inject({
method: 'GET',
url: `/v1/project-members?projectId=${mockProject.id}`,
headers: {
authorization: `Bearer ${mockToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
})
})
})
describe('Delete project member Endpoint', () => {
it('Deletes project member', async () => {
const { mockOwnerToken, mockProject, mockMember } = await createBasicEnvironment()
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.ADMIN }) as ProjectRole
const mockProjectMember = createMockProjectMember({
projectId: mockProject.id,
userId: mockMember.id,
projectRoleId: projectRole.id,
})
await databaseConnection()
.getRepository('project_member')
.save(mockProjectMember)
// act
const response = await app?.inject({
method: 'DELETE',
url: `/v1/project-members/${mockProjectMember.id}`,
headers: {
authorization: `Bearer ${mockOwnerToken}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.NO_CONTENT)
})
it.each([
DefaultProjectRole.EDITOR,
DefaultProjectRole.VIEWER,
DefaultProjectRole.OPERATOR,
])('Fails if user role is %s', async (testRole) => {
// arrange
const { mockPlatform, mockProject, mockMember } = await createBasicEnvironment()
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: testRole }) as ProjectRole
const mockProjectMember = createMockProjectMember({
userId: mockMember.id,
platformId: mockPlatform.id,
projectId: mockProject.id,
projectRoleId: projectRole.id,
})
await databaseConnection().getRepository('project_member').save([mockProjectMember])
const mockToken = await generateMockToken({
id: mockMember.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
// act
const response = await app?.inject({
method: 'DELETE',
url: `/v1/project-members/${mockProjectMember.id}`,
headers: {
authorization: `Bearer ${mockToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
const responseBody = response?.json()
expect(responseBody?.code).toBe('PERMISSION_DENIED')
expect(responseBody?.params?.userId).toBe(mockMember.id)
expect(responseBody?.params?.projectId).toBe(mockProject.id)
})
it('Delete project member from api', async () => {
const { mockApiKey, mockProject, mockMember } = await createBasicEnvironment()
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.ADMIN }) as ProjectRole
const mockProjectMember = createMockProjectMember({
projectId: mockProject.id,
userId: mockMember.id,
projectRoleId: projectRole.id,
})
await databaseConnection()
.getRepository('project_member')
.save(mockProjectMember)
// act
const response = await app?.inject({
method: 'DELETE',
url: `/v1/project-members/${mockProjectMember.id}`,
headers: {
authorization: `Bearer ${mockApiKey.value}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.NO_CONTENT)
})
it('Delete project member from api for non owner project', async () => {
const { mockApiKey, mockMember } = await createBasicEnvironment()
const { mockProject: mockProject2 } = await mockAndSaveBasicSetup({
plan: {
projectRolesEnabled: true,
auditLogEnabled: false,
},
})
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.ADMIN }) as ProjectRole
const mockProjectMember = createMockProjectMember({
projectId: mockProject2.id,
platformId: mockProject2.platformId,
userId: mockMember.id,
projectRoleId: projectRole.id,
})
await databaseConnection()
.getRepository('project_member')
.save(mockProjectMember)
// act
const response = await app?.inject({
method: 'DELETE',
url: `/v1/project-members/${mockProjectMember.id}`,
headers: {
authorization: `Bearer ${mockApiKey.value}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
})
})
async function createBasicEnvironment(): Promise<{
mockOwner: User
mockPlatform: Platform
mockProject: Project
mockApiKey: ApiKeyResponseWithValue
mockOwnerToken: string
mockMember: User
}> {
const { mockOwner, mockPlatform, mockProject, mockApiKey } = await mockAndSaveBasicSetupWithApiKey({
plan: {
projectRolesEnabled: true,
auditLogEnabled: false,
},
})
await databaseConnection().getRepository('user').update(mockOwner.id, {
platformId: mockPlatform.id,
platformRole: PlatformRole.ADMIN,
})
const mockOwnerToken = await generateMockToken({
id: mockOwner.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
const { mockUser: mockMember } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
return {
mockOwner,
mockPlatform,
mockProject,
mockApiKey,
mockOwnerToken,
mockMember,
}
}

View File

@@ -0,0 +1,56 @@
import {
CreateProjectReleaseRequestBody,
ProjectReleaseType,
} from '@activepieces/shared'
import { faker } from '@faker-js/faker'
import { FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
import {
createMockApiKey,
mockAndSaveBasicSetup,
} from '../../../helpers/mocks'
let app: FastifyInstance | null = null
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('Create Project Release', () => {
it('Fails if projectId does not match', async () => {
const { mockPlatform } = await mockAndSaveBasicSetup()
const apiKey = createMockApiKey({
platformId: mockPlatform.id,
})
await databaseConnection().getRepository('api_key').save([apiKey])
const request: CreateProjectReleaseRequestBody = {
name: faker.animal.bird(),
description: faker.lorem.sentence(),
selectedFlowsIds: [],
projectId: faker.string.uuid(),
type: ProjectReleaseType.GIT,
}
const response = await app?.inject({
method: 'POST',
url: '/v1/project-releases',
body: request,
headers: {
authorization: `Bearer ${apiKey.value}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
})

View File

@@ -0,0 +1,271 @@
import { PlatformRole, PrincipalType, ProjectRole, UpdateProjectRoleRequestBody } from '@activepieces/shared'
import { faker } from '@faker-js/faker'
import { FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
import { generateMockToken } from '../../../helpers/auth'
import { createMockProjectRole, mockAndSaveBasicSetup, mockBasicUser } from '../../../helpers/mocks'
let app: FastifyInstance | null = null
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('Project Role API', () => {
describe('Create Project Role', () => {
it('should create a new project role', async () => {
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockOwner.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
const projectRole = createMockProjectRole({ platformId: mockPlatform.id })
const response = await app?.inject({
method: 'POST',
url: '/v1/project-roles',
body: projectRole,
headers: {
authorization: `Bearer ${testToken}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.CREATED)
const responseBody = response?.json() as ProjectRole
expect(responseBody.id).toBeDefined()
expect(responseBody.platformId).toBe(mockPlatform.id)
expect(responseBody.name).toBe(projectRole.name)
expect(responseBody.permissions).toEqual(projectRole.permissions)
})
it('should fail to create a new project role if user is not platform owner', async () => {
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockUser.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
const projectRole = createMockProjectRole({ platformId: mockPlatform.id })
const response = await app?.inject({
method: 'POST',
url: '/v1/project-roles',
body: projectRole,
headers: {
authorization: `Bearer ${testToken}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
})
describe('Get Project Role', () => {
it('should get all project roles', async () => {
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockOwner.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
const response = await app?.inject({
method: 'GET',
url: '/v1/project-roles',
headers: {
authorization: `Bearer ${testToken}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.OK)
})
it('should able to get all project roles if user is not platform owner', async () => {
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockUser.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
const response = await app?.inject({
method: 'GET',
url: '/v1/project-roles',
headers: {
authorization: `Bearer ${testToken}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.OK)
})
})
describe('Update Project Role', () => {
it('should update a project role', async () => {
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockOwner.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
const projectRole = createMockProjectRole({ platformId: mockPlatform.id })
await databaseConnection().getRepository('project_role').save(projectRole)
const request: UpdateProjectRoleRequestBody = {
name: faker.lorem.word(),
permissions: ['read', 'write'],
}
const response = await app?.inject({
method: 'POST',
url: `/v1/project-roles/${projectRole.id}`,
body: request,
headers: {
authorization: `Bearer ${testToken}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.OK)
})
it('should fail to update if user is not platform owner', async () => {
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockUser.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
const projectRole = createMockProjectRole({ platformId: mockPlatform.id })
await databaseConnection().getRepository('project_role').save(projectRole)
const request: UpdateProjectRoleRequestBody = {
name: faker.lorem.word(),
permissions: ['read', 'write'],
}
const response = await app?.inject({
method: 'POST',
url: `/v1/project-roles/${projectRole.id}`,
body: request,
headers: {
authorization: `Bearer ${testToken}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
})
describe('Delete Project Role', () => {
it('should delete a project role', async () => {
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockOwner.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
const projectRole = createMockProjectRole({ platformId: mockPlatform.id })
await databaseConnection().getRepository('project_role').save(projectRole)
const response = await app?.inject({
method: 'DELETE',
url: `/v1/project-roles/${projectRole.name}`,
headers: {
authorization: `Bearer ${testToken}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.OK)
})
it('should fail to delete a project role if user is not platform owner', async () => {
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockUser.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
const projectRole = createMockProjectRole({ platformId: mockPlatform.id })
await databaseConnection().getRepository('project_role').save(projectRole)
const response = await app?.inject({
method: 'DELETE',
url: `/v1/project-roles/${projectRole.id}`,
headers: {
authorization: `Bearer ${testToken}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
it('should fail to delete a project role if project role does not exist', async () => {
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockOwner.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
const response = await app?.inject({
method: 'DELETE',
url: `/v1/project-roles/${faker.lorem.word()}`,
headers: {
authorization: `Bearer ${testToken}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.NOT_FOUND)
})
})
})

View File

@@ -0,0 +1,66 @@
import { ActivepiecesError, ErrorCode, FlowStatus } from '@activepieces/shared'
import { FastifyBaseLogger, FastifyInstance } from 'fastify'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { platformProjectService } from '../../../../src/app/ee/projects/platform-project-service'
import { setupServer } from '../../../../src/app/server'
import { createMockFlow, createMockFlowRun, createMockFlowVersion, mockAndSaveBasicSetup } from '../../../helpers/mocks'
let app: FastifyInstance | null = null
let mockLog: FastifyBaseLogger
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
mockLog = app!.log!
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('Platform Project Service', () => {
describe('Hard delete Project', () => {
it('Hard deletes associated flows fails', async () => {
// arrange
const { mockProject, mockPlatform } = await mockAndSaveBasicSetup()
const mockFlow = createMockFlow({ projectId: mockProject.id, status: FlowStatus.ENABLED })
await databaseConnection().getRepository('flow').save([mockFlow])
const mockFlowVersion = createMockFlowVersion({ flowId: mockFlow.id })
const mockPublishedFlowVersion = createMockFlowVersion({ flowId: mockFlow.id })
await databaseConnection().getRepository('flow_version').save([mockFlowVersion, mockPublishedFlowVersion])
const mockFlowRun = createMockFlowRun({
projectId: mockProject.id,
flowId: mockFlow.id,
flowVersionId: mockPublishedFlowVersion.id,
})
await databaseConnection().getRepository('flow_run').save([mockFlowRun])
await databaseConnection().getRepository('flow').update(mockFlow.id, {
publishedVersionId: mockPublishedFlowVersion.id,
})
try {
// act
await platformProjectService(mockLog).hardDelete({ id: mockProject.id, platformId: mockPlatform.id })
}
catch (error) {
// assert
expect(error).toBeInstanceOf(ActivepiecesError)
expect((error as ActivepiecesError).error.code).toBe(ErrorCode.VALIDATION)
return
}
throw new Error('Expected error to be thrown because project has enabled flows')
})
})
})

View File

@@ -0,0 +1,723 @@
import {
ApiKeyResponseWithValue,
UpdateProjectPlatformRequest,
} from '@activepieces/ee-shared'
import {
apId,
FlowStatus,
Platform,
PlatformRole,
PrincipalType,
Project,
User,
} from '@activepieces/shared'
import { faker } from '@faker-js/faker'
import { FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
import { generateMockToken } from '../../../helpers/auth'
import {
createMockApiKey,
createMockFlow,
createMockProject,
mockAndSaveBasicSetup,
mockBasicUser,
} from '../../../helpers/mocks'
let app: FastifyInstance | null = null
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('Project API', () => {
describe('Create Project', () => {
it('it should create project by user', async () => {
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockOwner.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
const displayName = faker.animal.bird()
const metadata = { foo: 'bar' }
const response = await app?.inject({
method: 'POST',
url: '/v1/projects',
body: {
displayName,
metadata,
},
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.CREATED)
expect(responseBody.displayName).toBe(displayName)
expect(responseBody.ownerId).toBe(mockOwner.id)
expect(responseBody.platformId).toBe(mockPlatform.id)
expect(responseBody.metadata).toEqual(metadata)
})
it('it should create project by api key', async () => {
const { mockOwner: mockUser, mockPlatform } = await mockAndSaveBasicSetup()
const apiKey = createMockApiKey({
platformId: mockPlatform.id,
})
await databaseConnection().getRepository('api_key').save([apiKey])
const displayName = faker.animal.bird()
const response = await app?.inject({
method: 'POST',
url: '/v1/projects',
body: {
displayName,
},
headers: {
authorization: `Bearer ${apiKey.value}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.CREATED)
const responseBody = response?.json()
expect(responseBody.displayName).toBe(displayName)
expect(responseBody.ownerId).toBe(mockUser.id)
expect(responseBody.platformId).toBe(mockPlatform.id)
})
})
describe('List Projects by api key', () => {
it('it should list platform project', async () => {
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
await mockAndSaveBasicSetup()
const apiKey = createMockApiKey({
platformId: mockPlatform.id,
})
await databaseConnection().getRepository('api_key').save([apiKey])
const response = await app?.inject({
method: 'GET',
url: '/v1/projects',
headers: {
authorization: `Bearer ${apiKey.value}`,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody.data.length).toBe(1)
expect(responseBody.data[0].id).toEqual(mockProject.id)
})
})
describe('List Projects by user', () => {
it('it should list owned projects in platform', async () => {
await mockAndSaveBasicSetup()
const { mockOwner: mockUserTwo, mockProject: mockProjectTwo, mockPlatform: mockPlatformTwo } = await mockAndSaveBasicSetup()
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockUserTwo.id,
projectId: mockProjectTwo.id,
platform: {
id: mockPlatformTwo.id,
},
})
const response = await app?.inject({
method: 'GET',
url: '/v1/users/projects',
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody.data.length).toBe(1)
expect(responseBody.data[0].id).toEqual(mockProjectTwo.id)
})
})
describe('Update Project', () => {
it('it should update project and ignore plan as project owner', async () => {
const { mockOwner: mockUser, mockPlatform } = await mockAndSaveBasicSetup()
mockUser.platformId = mockPlatform.id
mockUser.platformRole = PlatformRole.ADMIN
await databaseConnection().getRepository('user').save(mockUser)
const mockProject = createMockProject({
ownerId: mockUser.id,
platformId: mockPlatform.id,
})
await databaseConnection().getRepository('project').save([mockProject])
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockUser.id,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
const request: UpdateProjectPlatformRequest = {
displayName: faker.animal.bird(),
plan: {
},
}
const response = await app?.inject({
method: 'POST',
url: '/v1/projects/' + mockProject.id,
body: request,
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody.id).toBe(mockProject.id)
expect(responseBody.displayName).toBe(request.displayName)
})
it('it should update project as platform owner with api key', async () => {
const { mockProject, mockApiKey } =
await createProjectAndPlatformAndApiKey()
const request = {
displayName: faker.animal.bird(),
plan: {
},
}
const response = await app?.inject({
method: 'POST',
url: '/v1/projects/' + mockProject.id,
body: request,
headers: {
authorization: `Bearer ${mockApiKey.value}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.OK)
})
it('it should update project as platform owner', async () => {
const { mockProject, mockPlatform, mockUser } =
await createProjectAndPlatformAndApiKey()
const mockProjectTwo = createMockProject({
ownerId: mockUser.id,
platformId: mockPlatform.id,
})
await databaseConnection()
.getRepository('project')
.save([mockProject, mockProjectTwo])
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockUser.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
const request: UpdateProjectPlatformRequest = {
displayName: faker.animal.bird(),
plan: {
},
}
const response = await app?.inject({
method: 'POST',
url: '/v1/projects/' + mockProjectTwo.id,
body: request,
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
const responseBody = response?.json()
expect(responseBody.displayName).toBe(request.displayName)
})
it('Fails if user is not platform owner', async () => {
const { mockOwner: platformOwnerUser, mockPlatform } = await mockAndSaveBasicSetup()
const { mockUser: memberUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const mockProject = createMockProject({
ownerId: platformOwnerUser.id,
platformId: mockPlatform.id,
})
const mockProjectTwo = createMockProject({
ownerId: platformOwnerUser.id,
platformId: mockPlatform.id,
})
await databaseConnection()
.getRepository('project')
.save([mockProject, mockProjectTwo])
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: memberUser.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
const request: UpdateProjectPlatformRequest = {
displayName: faker.animal.bird(),
}
const response = await app?.inject({
method: 'POST',
url: '/v1/projects/' + mockProjectTwo.id,
body: request,
headers: {
authorization: `Bearer ${testToken}`,
},
})
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
it('Fails if project is deleted', async () => {
// arrange
const { mockOwner, mockProject } = await mockAndSaveBasicSetup({
project: {
deleted: new Date().toISOString(),
},
})
const mockToken = await generateMockToken({
id: mockOwner.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockProject.platformId,
},
})
const request: UpdateProjectPlatformRequest = {
displayName: faker.animal.bird(),
}
// act
const response = await app?.inject({
method: 'POST',
url: `/v1/projects/${mockProject.id}`,
body: request,
headers: {
authorization: `Bearer ${mockToken}`,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.NOT_FOUND)
expect(responseBody?.code).toBe('ENTITY_NOT_FOUND')
expect(responseBody?.params?.entityId).toBe(mockProject.id)
expect(responseBody?.params?.entityType).toBe('project')
})
it('it should update project with metadata', async () => {
const { mockOwner: mockUser, mockPlatform } = await mockAndSaveBasicSetup()
mockUser.platformId = mockPlatform.id
mockUser.platformRole = PlatformRole.ADMIN
await databaseConnection().getRepository('user').save(mockUser)
const mockProject = createMockProject({
ownerId: mockUser.id,
platformId: mockPlatform.id,
})
await databaseConnection().getRepository('project').save([mockProject])
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockUser.id,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
const metadata = { foo: 'bar' }
const request: UpdateProjectPlatformRequest = {
displayName: faker.animal.bird(),
metadata,
plan: {
},
}
const response = await app?.inject({
method: 'POST',
url: '/v1/projects/' + mockProject.id,
body: request,
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody.id).toBe(mockProject.id)
expect(responseBody.displayName).toBe(request.displayName)
expect(responseBody.metadata).toEqual(metadata)
})
})
describe('Delete Project endpoint', () => {
it('Soft deletes project by id', async () => {
// arrange
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const mockProjectToDelete = createMockProject({ ownerId: mockOwner.id, platformId: mockPlatform.id })
await databaseConnection().getRepository('project').save([mockProjectToDelete])
const mockToken = await generateMockToken({
id: mockOwner.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockProject.platformId,
},
})
// act
const response = await app?.inject({
method: 'DELETE',
url: `/v1/projects/${mockProjectToDelete.id}`,
headers: {
authorization: `Bearer ${mockToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.NO_CONTENT)
const deletedProject = await databaseConnection().getRepository('project').findOneBy({ id: mockProjectToDelete.id })
expect(deletedProject?.deleted).not.toBeNull()
})
it('Fails if project has enabled flows', async () => {
// arrange
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const mockProjectToDelete = createMockProject({ ownerId: mockOwner.id, platformId: mockPlatform.id })
await databaseConnection().getRepository('project').save([mockProjectToDelete])
const enabledFlow = createMockFlow({ projectId: mockProjectToDelete.id, status: FlowStatus.ENABLED })
await databaseConnection().getRepository('flow').save([enabledFlow])
const mockToken = await generateMockToken({
id: mockOwner.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockProject.platformId,
},
})
// act
const response = await app?.inject({
method: 'DELETE',
url: `/v1/projects/${mockProjectToDelete.id}`,
headers: {
authorization: `Bearer ${mockToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.CONFLICT)
const responseBody = response?.json()
expect(responseBody?.code).toBe('VALIDATION')
expect(responseBody?.params?.message).toBe('PROJECT_HAS_ENABLED_FLOWS')
})
it('Fails if project to delete is the active project', async () => {
// arrange
const { mockOwner, mockProject } = await mockAndSaveBasicSetup()
const mockToken = await generateMockToken({
id: mockOwner.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockProject.platformId,
},
})
// act
const response = await app?.inject({
method: 'DELETE',
url: `/v1/projects/${mockProject.id}`,
headers: {
authorization: `Bearer ${mockToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.CONFLICT)
const responseBody = response?.json()
expect(responseBody?.code).toBe('VALIDATION')
expect(responseBody?.params?.message).toBe('ACTIVE_PROJECT')
})
it('Requires user to be platform owner', async () => {
// arrange
const { mockOwner, mockProject } = await mockAndSaveBasicSetup()
await databaseConnection().getRepository('user').update(mockOwner.id, {
platformRole: PlatformRole.MEMBER,
})
const mockToken = await generateMockToken({
id: mockOwner.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockProject.platformId,
},
})
// act
const response = await app?.inject({
method: 'DELETE',
url: `/v1/projects/${mockProject.id}`,
headers: {
authorization: `Bearer ${mockToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
const responseBody = response?.json()
expect(responseBody?.code).toBe('AUTHORIZATION')
})
it('Fails if project to delete is not in current platform', async () => {
// arrange
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const mockProjectToDelete = createMockProject({ ownerId: mockOwner.id, platformId: mockPlatform.id })
await databaseConnection().getRepository('project').save([mockProjectToDelete])
const randomPlatformId = apId()
const mockToken = await generateMockToken({
id: mockOwner.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: randomPlatformId,
},
})
// act
const response = await app?.inject({
method: 'DELETE',
url: `/v1/projects/${mockProjectToDelete.id}`,
headers: {
authorization: `Bearer ${mockToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
})
describe('Platform Operator Access', () => {
it('Platform operator can access all projects in their platform', async () => {
// arrange
const { mockOwner, mockPlatform } = await mockAndSaveBasicSetup()
// Create a platform operator user
const { mockUser: operatorUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.OPERATOR,
},
})
// Create multiple projects owned by different users
const project1 = createMockProject({
ownerId: mockOwner.id,
platformId: mockPlatform.id,
displayName: 'Project 1',
})
const project2 = createMockProject({
ownerId: mockOwner.id,
platformId: mockPlatform.id,
displayName: 'Project 2',
})
await databaseConnection().getRepository('project').save([project1, project2])
const operatorToken = await generateMockToken({
type: PrincipalType.USER,
id: operatorUser.id,
projectId: project1.id,
platform: { id: mockPlatform.id },
})
// act - list projects
const response = await app?.inject({
method: 'GET',
url: '/v1/users/projects',
headers: {
authorization: `Bearer ${operatorToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
const responseBody = response?.json()
// Platform operator should see all projects including the default one
expect(responseBody.data.length).toBeGreaterThanOrEqual(2)
const projectNames = responseBody.data.map((p: Project) => p.displayName)
expect(projectNames).toContain('Project 1')
expect(projectNames).toContain('Project 2')
})
it('Platform operator cannot update platform settings', async () => {
// arrange
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const { mockUser: operatorUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.OPERATOR,
},
})
const operatorToken = await generateMockToken({
type: PrincipalType.USER,
id: operatorUser.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
// act - try to update platform
const response = await app?.inject({
method: 'POST',
url: `/v1/platforms/${mockPlatform.id}`,
headers: {
authorization: `Bearer ${operatorToken}`,
},
body: {
name: 'Should not be allowed',
},
})
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
it('Platform member cannot access projects they are not member of', async () => {
// arrange
const { mockOwner, mockPlatform } = await mockAndSaveBasicSetup()
// Create a regular platform member
const { mockUser: memberUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
// Create a project the member is NOT part of
const project = createMockProject({
ownerId: mockOwner.id,
platformId: mockPlatform.id,
displayName: 'Restricted Project',
})
await databaseConnection().getRepository('project').save(project)
const memberToken = await generateMockToken({
type: PrincipalType.USER,
id: memberUser.id,
projectId: project.id,
platform: { id: mockPlatform.id },
})
// act - list projects
const response = await app?.inject({
method: 'GET',
url: '/v1/users/projects',
headers: {
authorization: `Bearer ${memberToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
const responseBody = response?.json()
expect(responseBody.data).toHaveLength(0) // Should not see any projects
})
})
})
async function createProjectAndPlatformAndApiKey(): Promise<{
mockApiKey: ApiKeyResponseWithValue
mockPlatform: Platform
mockProject: Project
mockUser: User
}> {
const { mockOwner: mockUser, mockPlatform } = await mockAndSaveBasicSetup()
mockUser.platformId = mockPlatform.id
mockUser.platformRole = PlatformRole.ADMIN
await databaseConnection().getRepository('user').save(mockUser)
const mockProject = createMockProject({
ownerId: mockUser.id,
platformId: mockPlatform.id,
})
await databaseConnection().getRepository('project').save(mockProject)
const mockApiKey = createMockApiKey({
platformId: mockPlatform.id,
})
await databaseConnection().getRepository('api_key').save(mockApiKey)
return {
mockApiKey,
mockPlatform,
mockProject,
mockUser,
}
}

View File

@@ -0,0 +1,235 @@
import { PlatformRole, PrincipalType } from '@activepieces/shared'
import { faker } from '@faker-js/faker'
import { FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
import { generateMockToken } from '../../../helpers/auth'
import {
createMockSigningKey,
mockAndSaveBasicSetup,
mockBasicUser,
} from '../../../helpers/mocks'
let app: FastifyInstance | null = null
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
const setupEnabledPlatform = () => mockAndSaveBasicSetup({ plan: { embeddingEnabled: true } })
describe('Signing Key API', () => {
describe('Add Signing Key API', () => {
it('Creates new Signing Key', async () => {
// arrange
const { mockOwner, mockPlatform, mockProject } = await setupEnabledPlatform()
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockOwner.id,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
const mockSigningKeyName = faker.lorem.word()
const response = await app?.inject({
method: 'POST',
url: '/v1/signing-keys',
body: {
displayName: mockSigningKeyName,
},
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.CREATED)
expect(responseBody.id).toHaveLength(21)
expect(responseBody.platformId).toBe(mockPlatform.id)
expect(responseBody.publicKey).toBeDefined()
expect(responseBody.displayName).toBe(mockSigningKeyName)
expect(responseBody.privateKey).toBeDefined()
expect(responseBody.algorithm).toBe('RSA')
}, 10000)
it('Fails if user is not platform owner', async () => {
// arrange
const { mockPlatform, mockProject } = await setupEnabledPlatform()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const mockSigningKey = createMockSigningKey({
platformId: mockPlatform.id,
})
await databaseConnection()
.getRepository('signing_key')
.save(mockSigningKey)
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockUser.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
const mockSigningKeyName = faker.lorem.word()
const response = await app?.inject({
method: 'POST',
url: '/v1/signing-keys',
body: {
displayName: mockSigningKeyName,
},
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
})
describe('Get Signing Key endpoint', () => {
it('Finds a Signing Key by id', async () => {
// arrange
const { mockOwner, mockPlatform, mockProject } = await setupEnabledPlatform()
const mockSigningKey = createMockSigningKey({
platformId: mockPlatform.id,
})
await databaseConnection()
.getRepository('signing_key')
.save(mockSigningKey)
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockOwner.id,
projectId: mockProject.id,
platform: { id: mockPlatform.id },
})
// act
const response = await app?.inject({
method: 'GET',
url: `/v1/signing-keys/${mockSigningKey.id}`,
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody.id).toBe(mockSigningKey.id)
expect(responseBody.platformId).toBe(mockSigningKey.platformId)
expect(responseBody.publicKey).toBe(mockSigningKey.publicKey)
expect(responseBody.algorithm).toBe(mockSigningKey.algorithm)
})
})
describe('Delete Signing Key endpoint', () => {
it('Fail if non owner', async () => {
// arrange
const { mockPlatform: mockPlatformOne, mockProject: mockProjectOne } = await setupEnabledPlatform()
const { mockUser: nonOwnerUser } = await mockBasicUser({
user: {
platformId: mockPlatformOne.id,
platformRole: PlatformRole.MEMBER,
},
})
const mockSigningKey = createMockSigningKey({
platformId: mockPlatformOne.id,
})
await databaseConnection()
.getRepository('signing_key')
.save(mockSigningKey)
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: nonOwnerUser.id,
projectId: mockProjectOne.id,
platform: {
id: mockPlatformOne.id,
},
})
// act
const response = await app?.inject({
method: 'DELETE',
url: `/v1/signing-keys/${mockSigningKey.id}`,
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
})
describe('List Signing Keys endpoint', () => {
it('Filters Signing Keys by platform', async () => {
const { mockPlatform: mockPlatformTwo, mockProject: mockProjectTwo } = await setupEnabledPlatform()
const { mockOwner: mockUserOne, mockPlatform: mockPlatformOne } = await setupEnabledPlatform()
const mockSigningKeyOne = createMockSigningKey({
platformId: mockPlatformOne.id,
})
const mockSigningKeyTwo = createMockSigningKey({
platformId: mockPlatformTwo.id,
})
await databaseConnection()
.getRepository('signing_key')
.save([mockSigningKeyOne, mockSigningKeyTwo])
const testToken = await generateMockToken({
type: PrincipalType.USER,
id: mockUserOne.id,
projectId: mockProjectTwo.id,
platform: {
id: mockPlatformOne.id,
},
})
// act
const response = await app?.inject({
method: 'GET',
url: '/v1/signing-keys',
headers: {
authorization: `Bearer ${testToken}`,
},
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody.data).toHaveLength(1)
expect(responseBody.data[0].id).toBe(mockSigningKeyOne.id)
})
})
})

View File

@@ -0,0 +1,168 @@
import { apId, PlatformRole, PrincipalType, User } from '@activepieces/shared'
import { FastifyInstance, LightMyRequestResponse } from 'fastify'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
import { generateMockToken } from '../../../helpers/auth'
import { mockAndSaveBasicSetup, mockBasicUser } from '../../../helpers/mocks'
let app: FastifyInstance | null = null
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('Store-entries API', () => {
const projectId = apId()
let engineToken: string
let userToken: string
let serviceToken: string
let mockUser: User
beforeEach(async () => {
const { mockPlatform } = await mockAndSaveBasicSetup()
const { mockUser: user } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
mockUser = user
engineToken = await generateMockToken({
type: PrincipalType.ENGINE,
id: apId(),
projectId,
platform: {
id: mockPlatform.id,
},
})
userToken = await generateMockToken({
type: PrincipalType.USER,
id: mockUser.id,
projectId,
platform: {
id: mockPlatform.id,
},
})
serviceToken = await generateMockToken({
type: PrincipalType.SERVICE,
id: apId(),
projectId,
platform: {
id: mockPlatform.id,
},
})
})
describe('POST /v1/store-entries', () => {
it('should handle token type engineToken correctly and return 200', async () => {
const key = 'new_key_1'
const response = await makePostRequest(engineToken, key, 'random_value_0')
expect(response?.statusCode).toBe(200)
})
it('should handle token type userToken correctly and return 200', async () => {
const key = 'new_key_2'
const response = await makePostRequest(userToken, key, 'random_value_0')
expect(response?.statusCode).toBe(200)
})
it('should handle token type serviceToken correctly and return 401', async () => {
const key = 'new_key_3'
const response = await makePostRequest(serviceToken, key, 'random_value_0')
expect(response?.statusCode).toBe(403)
})
it('should save and update the value', async () => {
const key = 'new_key_1'
let response = await makePostRequest(engineToken, key, 'random_value_0')
const firstResponseBody = response?.json()
expect(response?.statusCode).toBe(200)
expect(firstResponseBody.key).toEqual(key)
expect(firstResponseBody.projectId).toEqual(projectId)
expect(firstResponseBody.value).toEqual('random_value_0')
response = await makePostRequest(engineToken, key, 'random_value_1')
const secondResponseBody = response?.json()
expect(response?.statusCode).toBe(200)
expect(secondResponseBody.key).toEqual(key)
expect(secondResponseBody.projectId).toEqual(projectId)
expect(secondResponseBody.value).toEqual('random_value_1')
expect(firstResponseBody.created).toEqual(secondResponseBody.created)
})
it('should return saved value', async () => {
const key = 'new_key_2'
let response = await makePostRequest(engineToken, key, 'random_value_2')
expect(response?.statusCode).toBe(200)
const saveResponse = response?.json()
response = await makeGetRequest(engineToken, key)
expect(response?.statusCode).toBe(200)
const getResponse = response?.json()
expect(getResponse.key).toEqual(saveResponse.key)
expect(getResponse.value).toEqual(saveResponse.value)
expect(getResponse.projectId).toEqual(saveResponse.projectId)
})
it('should delete saved value', async () => {
const key = 'new_key_3'
let response = await makePostRequest(engineToken, key, 'random_value_3')
expect(response?.statusCode).toBe(200)
response = await makeDeleteRequest(engineToken, key)
expect(response?.statusCode).toBe(200)
response = await makeGetRequest(engineToken, key)
expect(response?.statusCode).toBe(404)
})
})
})
function makePostRequest(testToken: string, key: string, value: string): Promise<LightMyRequestResponse> | undefined {
return app?.inject({
method: 'POST',
url: '/v1/store-entries/',
headers: {
authorization: `Bearer ${testToken}`,
},
body: {
key,
value,
},
})
}
function makeGetRequest(testToken: string, key: string): Promise<LightMyRequestResponse> | undefined {
return app?.inject({
method: 'GET',
url: `/v1/store-entries/?key=${key}`,
headers: {
authorization: `Bearer ${testToken}`,
},
})
}
function makeDeleteRequest(testToken: string, key: string): Promise<LightMyRequestResponse> | undefined {
return app?.inject({
method: 'DELETE',
url: `/v1/store-entries/?key=${key}`,
headers: {
authorization: `Bearer ${testToken}`,
},
})
}

View File

@@ -0,0 +1,523 @@
import {
ApiKeyResponseWithValue,
} from '@activepieces/ee-shared'
import { DefaultProjectRole, InvitationStatus, InvitationType, Platform, PlatformRole, PrincipalType, Project, ProjectRole, ProjectType, SendUserInvitationRequest, User } from '@activepieces/shared'
import { faker } from '@faker-js/faker'
import { FastifyBaseLogger, FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { emailService } from '../../../../src/app/ee/helper/email/email-service'
import { setupServer } from '../../../../src/app/server'
import { generateMockToken } from '../../../helpers/auth'
import {
createMockProjectMember,
createMockUserInvitation,
mockAndSaveBasicSetupWithApiKey,
mockBasicUser,
} from '../../../helpers/mocks'
let app: FastifyInstance | null = null
let mockLog: FastifyBaseLogger
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
mockLog = app!.log!
})
beforeEach(async () => {
emailService(mockLog).sendInvitation = jest.fn()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('User Invitation API', () => {
describe('Invite User', () => {
it('should return invitation link when smtp is not configured', async () => {
const { mockApiKey, mockProject } = await createBasicEnvironment({})
const mockInviteProjectMemberRequest: SendUserInvitationRequest = {
email: faker.internet.email(),
type: InvitationType.PLATFORM,
platformRole: PlatformRole.ADMIN,
}
const response = await app?.inject({
method: 'POST',
url: '/v1/user-invitations',
headers: {
authorization: `Bearer ${mockApiKey.value}`,
},
query: {
projectId: mockProject.id,
},
body: mockInviteProjectMemberRequest,
})
expect(response?.statusCode).toBe(StatusCodes.CREATED)
const responseBody = response?.json()
expect(responseBody?.link).toBeUndefined()
const invitationId = responseBody?.id
const invitation = await databaseConnection().getRepository('user_invitation').findOneBy({ id: invitationId })
expect(invitation?.status).toBe(InvitationStatus.ACCEPTED)
})
it('should have status pending when inviting a user', async () => {
const { mockOwnerToken, mockProject } = await createBasicEnvironment({})
const mockInviteProjectMemberRequest: SendUserInvitationRequest = {
email: faker.internet.email(),
type: InvitationType.PLATFORM,
platformRole: PlatformRole.ADMIN,
}
const response = await app?.inject({
method: 'POST',
url: '/v1/user-invitations',
headers: {
authorization: `Bearer ${mockOwnerToken}`,
},
query: {
projectId: mockProject.id,
},
body: mockInviteProjectMemberRequest,
})
expect(response?.statusCode).toBe(StatusCodes.CREATED)
const responseBody = response?.json()
const invitationId = responseBody?.id
const invitation = await databaseConnection().getRepository('user_invitation').findOneBy({ id: invitationId })
expect(invitation?.status).toBe(InvitationStatus.PENDING)
})
it('Invite user to Platform Member', async () => {
const { mockApiKey, mockProject } = await createBasicEnvironment({})
const mockInviteProjectMemberRequest: SendUserInvitationRequest = {
email: faker.internet.email(),
type: InvitationType.PLATFORM,
platformRole: PlatformRole.ADMIN,
}
const response = await app?.inject({
method: 'POST',
url: '/v1/user-invitations',
headers: {
authorization: `Bearer ${mockApiKey.value}`,
},
query: {
projectId: mockProject.id,
},
body: mockInviteProjectMemberRequest,
})
expect(response?.statusCode).toBe(StatusCodes.CREATED)
})
it('Invite user to other platform project should fail', async () => {
const { mockApiKey } = await createBasicEnvironment({})
const { mockProject: mockProject2 } = await createBasicEnvironment({})
const adminRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.ADMIN }) as ProjectRole
const mockInviteProjectMemberRequest: SendUserInvitationRequest = {
projectRole: adminRole.name,
email: faker.internet.email(),
projectId: mockProject2.id,
type: InvitationType.PROJECT,
}
const response = await app?.inject({
method: 'POST',
url: '/v1/user-invitations',
headers: {
authorization: `Bearer ${mockApiKey.value}`,
},
body: mockInviteProjectMemberRequest,
})
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
const responseBody = response?.json()
expect(responseBody?.code).toBe('AUTHORIZATION')
})
it('should reject invitation to personal project', async () => {
const { mockApiKey, mockProject } = await createBasicEnvironment({
project: {
type: ProjectType.PERSONAL,
},
})
const adminRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.ADMIN }) as ProjectRole
const mockInviteProjectMemberRequest: SendUserInvitationRequest = {
projectRole: adminRole.name,
email: faker.internet.email(),
projectId: mockProject.id,
type: InvitationType.PROJECT,
}
const response = await app?.inject({
method: 'POST',
url: '/v1/user-invitations',
headers: {
authorization: `Bearer ${mockApiKey.value}`,
},
body: mockInviteProjectMemberRequest,
})
expect(response?.statusCode).toBe(StatusCodes.CONFLICT)
const responseBody = response?.json()
expect(responseBody?.code).toBe('VALIDATION')
expect(responseBody?.params?.message).toBe('Project must be a team project')
})
it('Invite user to Project Member using api key', async () => {
const { mockApiKey, mockProject } = await createBasicEnvironment({})
const adminRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.ADMIN }) as ProjectRole
const mockInviteProjectMemberRequest: SendUserInvitationRequest = {
projectRole: adminRole.name,
email: faker.internet.email(),
projectId: mockProject.id,
type: InvitationType.PROJECT,
}
const response = await app?.inject({
method: 'POST',
url: '/v1/user-invitations',
headers: {
authorization: `Bearer ${mockApiKey.value}`,
},
body: mockInviteProjectMemberRequest,
})
expect(response?.statusCode).toBe(StatusCodes.CREATED)
})
it('Invite user to Project Member', async () => {
const { mockOwnerToken, mockProject } = await createBasicEnvironment({})
const adminRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.ADMIN }) as ProjectRole
const mockInviteProjectMemberRequest: SendUserInvitationRequest = {
projectRole: adminRole.name,
email: faker.internet.email(),
projectId: mockProject.id,
type: InvitationType.PROJECT,
}
const response = await app?.inject({
method: 'POST',
url: '/v1/user-invitations',
headers: {
authorization: `Bearer ${mockOwnerToken}`,
},
body: mockInviteProjectMemberRequest,
})
expect(response?.statusCode).toBe(StatusCodes.CREATED)
})
it.each([
DefaultProjectRole.EDITOR,
DefaultProjectRole.VIEWER,
DefaultProjectRole.OPERATOR,
])('Fails if user role is %s', async (testRole) => {
const { mockMember, mockProject } = await createBasicEnvironment({})
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: testRole }) as ProjectRole
const mockInviteProjectMemberRequest: SendUserInvitationRequest = {
projectRole: projectRole.name,
email: faker.internet.email(),
projectId: mockProject.id,
type: InvitationType.PROJECT,
}
const mockProjectMember = createMockProjectMember({
userId: mockMember.id,
platformId: mockMember.platformId!,
projectId: mockProject.id,
projectRoleId: projectRole.id,
})
await databaseConnection().getRepository('project_member').save(mockProjectMember)
const mockToken = await generateMockToken({
id: mockMember.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockProject.platformId,
},
})
const response = await app?.inject({
method: 'POST',
url: '/v1/user-invitations',
headers: {
authorization: `Bearer ${mockToken}`,
},
body: mockInviteProjectMemberRequest,
})
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
const responseBody = response?.json()
expect(responseBody?.code).toBe('PERMISSION_DENIED')
})
})
describe('List User Invitations', () => {
it('should succeed', async () => {
const { mockOwnerToken, mockPlatform, mockProject } = await createBasicEnvironment({})
const adminRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.ADMIN }) as ProjectRole
const mockUserInvitation = createMockUserInvitation({
email: faker.internet.email(),
platformId: mockPlatform.id,
projectId: mockProject.id,
type: InvitationType.PROJECT,
projectRole: adminRole,
})
await databaseConnection().getRepository('user_invitation').save(mockUserInvitation)
const listResponse = await app?.inject({
method: 'GET',
url: '/v1/user-invitations',
query: {
type: InvitationType.PROJECT,
},
headers: {
authorization: `Bearer ${mockOwnerToken}`,
},
})
const responseBody = listResponse?.json()
expect(listResponse?.statusCode).toBe(StatusCodes.OK)
expect(responseBody?.data.length).toBe(1)
})
it('should succeed with API key', async () => {
const { mockApiKey, mockPlatform, mockProject } = await createBasicEnvironment({})
const adminRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.ADMIN }) as ProjectRole
const mockUserInvitation = createMockUserInvitation({
email: faker.internet.email(),
platformId: mockPlatform.id,
projectId: mockProject.id,
type: InvitationType.PROJECT,
status: InvitationStatus.PENDING,
projectRole: adminRole,
})
await databaseConnection().getRepository('user_invitation').save(mockUserInvitation)
const listResponse = await app?.inject({
method: 'GET',
url: '/v1/user-invitations',
query: {
type: InvitationType.PROJECT,
},
headers: {
authorization: `Bearer ${mockApiKey.value}`,
},
})
const responseBody = listResponse?.json()
expect(listResponse?.statusCode).toBe(StatusCodes.OK)
expect(responseBody?.data.length).toBe(1)
})
it('should return empty list with API key from another platform', async () => {
const { mockPlatform, mockProject } = await createBasicEnvironment({})
const adminRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.ADMIN }) as ProjectRole
const mockUserInvitation = createMockUserInvitation({
email: faker.internet.email(),
platformId: mockPlatform.id,
projectId: mockProject.id,
type: InvitationType.PROJECT,
projectRole: adminRole,
})
await databaseConnection().getRepository('user_invitation').save(mockUserInvitation)
const { mockApiKey: anotherApiKey } = await createBasicEnvironment({})
const listResponse = await app?.inject({
method: 'GET',
url: '/v1/user-invitations',
query: {
type: InvitationType.PROJECT,
},
headers: {
authorization: `Bearer ${anotherApiKey.value}`,
},
})
const responseBody = listResponse?.json()
expect(listResponse?.statusCode).toBe(StatusCodes.OK)
expect(responseBody?.data.length).toBe(0)
})
it('should return forbidden when listing invitations for a project owned by another platform using API key', async () => {
// Create two separate environments
const { mockApiKey: apiKey1 } = await createBasicEnvironment({})
const { mockProject: project2 } = await createBasicEnvironment({})
const adminRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.ADMIN }) as ProjectRole
const mockUserInvitation = createMockUserInvitation({
email: faker.internet.email(),
platformId: project2.platformId,
projectId: project2.id,
type: InvitationType.PROJECT,
status: InvitationStatus.PENDING,
projectRole: adminRole,
})
await databaseConnection().getRepository('user_invitation').save(mockUserInvitation)
// Attempt to list invitations for project2 using apiKey1
const listResponse = await app?.inject({
method: 'GET',
url: '/v1/user-invitations',
query: {
projectId: project2.id,
type: InvitationType.PROJECT,
},
headers: {
authorization: `Bearer ${apiKey1.value}`,
},
})
expect(listResponse?.statusCode).toBe(StatusCodes.FORBIDDEN)
const responseBody = listResponse?.json()
expect(responseBody?.code).toBe('AUTHORIZATION')
})
it.each([
DefaultProjectRole.EDITOR,
DefaultProjectRole.VIEWER,
DefaultProjectRole.ADMIN,
DefaultProjectRole.OPERATOR,
])('Succeed if user role is %s', async (testRole) => {
const { mockMember, mockProject } = await createBasicEnvironment({})
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: testRole }) as ProjectRole
const mockInviteProjectMemberRequest: SendUserInvitationRequest = {
projectRole: projectRole.name,
email: faker.internet.email(),
projectId: mockProject.id,
type: InvitationType.PROJECT,
}
const mockProjectMember = createMockProjectMember({
userId: mockMember.id,
platformId: mockMember.platformId!,
projectId: mockProject.id,
projectRoleId: projectRole.id,
})
await databaseConnection().getRepository('project_member').save(mockProjectMember)
const mockToken = await generateMockToken({
id: mockMember.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockProject.platformId,
},
})
const response = await app?.inject({
method: 'GET',
url: '/v1/user-invitations',
headers: {
authorization: `Bearer ${mockToken}`,
},
query: {
type: InvitationType.PROJECT,
},
body: mockInviteProjectMemberRequest,
})
expect(response?.statusCode).toBe(StatusCodes.OK)
})
})
describe('Delete User Invitation', () => {
it('Delete User Invitation', async () => {
const { mockOwnerToken, mockPlatform } = await createBasicEnvironment({})
const mockUserInvitation = createMockUserInvitation({
email: faker.internet.email(),
platformId: mockPlatform.id,
type: InvitationType.PLATFORM,
platformRole: PlatformRole.ADMIN,
})
await databaseConnection().getRepository('user_invitation').save(mockUserInvitation)
const deleteResponse = await app?.inject({
method: 'DELETE',
url: `/v1/user-invitations/${mockUserInvitation.id}`,
headers: {
authorization: `Bearer ${mockOwnerToken}`,
},
})
expect(deleteResponse?.statusCode).toBe(StatusCodes.NO_CONTENT)
})
it('Delete User Invitation with API key', async () => {
const { mockApiKey, mockPlatform } = await createBasicEnvironment({})
const mockUserInvitation = createMockUserInvitation({
email: faker.internet.email(),
platformId: mockPlatform.id,
type: InvitationType.PLATFORM,
platformRole: PlatformRole.ADMIN,
})
await databaseConnection().getRepository('user_invitation').save(mockUserInvitation)
const deleteResponse = await app?.inject({
method: 'DELETE',
url: `/v1/user-invitations/${mockUserInvitation.id}`,
headers: {
authorization: `Bearer ${mockApiKey.value}`,
},
})
expect(deleteResponse?.statusCode).toBe(StatusCodes.NO_CONTENT)
})
})
})
async function createBasicEnvironment({ platform, project }: { platform?: Partial<Platform>, project?: Partial<Project> }): Promise<{
mockOwner: User
mockPlatform: Platform
mockProject: Project
mockApiKey: ApiKeyResponseWithValue
mockOwnerToken: string
mockMember: User
}> {
const { mockOwner, mockPlatform, mockProject, mockApiKey } = await mockAndSaveBasicSetupWithApiKey({
platform: {
...platform,
},
project: {
...project,
},
plan: {
projectRolesEnabled: true,
auditLogEnabled: false,
},
})
const mockOwnerToken = await generateMockToken({
id: mockOwner.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
const { mockUser: mockMember } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
return {
mockOwner,
mockPlatform,
mockProject,
mockApiKey,
mockOwnerToken,
mockMember,
}
}

View File

@@ -0,0 +1,235 @@
import {
PlatformRole,
PrincipalType,
UserStatus,
} from '@activepieces/shared'
import { FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
import { generateMockToken } from '../../../helpers/auth'
import {
mockAndSaveBasicSetup,
mockAndSaveBasicSetupWithApiKey,
mockBasicUser,
} from '../../../helpers/mocks'
let app: FastifyInstance | null = null
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('Enterprise User API', () => {
describe('List users endpoint', () => {
it('Allows service accounts', async () => {
// arrange
const { mockOwner, mockPlatform, mockApiKey } = await mockAndSaveBasicSetupWithApiKey()
// act
const response = await app?.inject({
method: 'GET',
url: '/v1/users',
query: {
platformId: mockPlatform.id,
},
headers: {
authorization: `Bearer ${mockApiKey.value}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
const responseBody = response?.json()
expect(Object.keys(responseBody)).toHaveLength(3)
expect(responseBody.data).toHaveLength(1)
expect(responseBody.data[0].id).toBe(mockOwner.id)
})
})
describe('Update user endpoint', () => {
it('Failed if own other platform', async () => {
// arrange
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const { mockOwner: mockOwner2, mockPlatform: mockPlatform2 } = await mockAndSaveBasicSetup()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
status: UserStatus.ACTIVE,
},
})
const mockUserToken = await generateMockToken({
id: mockOwner2.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform2.id,
},
})
const response = await app?.inject({
method: 'POST',
url: `/v1/users/${mockUser.id}`,
headers: {
authorization: `Bearer ${mockUserToken}`,
},
body: {
status: UserStatus.INACTIVE,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.NOT_FOUND)
})
it('Fail if not admin', async () => {
// arrange
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
status: UserStatus.ACTIVE,
},
})
const mockUserToken = await generateMockToken({
id: mockUser.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
// act
const response = await app?.inject({
method: 'POST',
url: `/v1/users/${mockUser.id}`,
headers: {
authorization: `Bearer ${mockUserToken}`,
},
body: {
status: UserStatus.INACTIVE,
},
})
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
it('Allows service accounts to activate', async () => {
// arrange
const { mockPlatform, mockApiKey } = await mockAndSaveBasicSetupWithApiKey()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
status: UserStatus.INACTIVE,
},
})
// act
const response = await app?.inject({
method: 'POST',
url: `/v1/users/${mockUser.id}`,
headers: {
authorization: `Bearer ${mockApiKey.value}`,
},
body: {
status: UserStatus.ACTIVE,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
const responseJson = response?.json()
expect(responseJson.id).toBe(mockUser.id)
expect(responseJson.password).toBeUndefined()
expect(responseJson.status).toBe(UserStatus.ACTIVE)
})
})
describe('Delete user endpoint', () => {
it('Fails if user is not platform owner', async () => {
// arrange
const { mockPlatform, mockProject } = await mockAndSaveBasicSetupWithApiKey()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const mockUserToken = await generateMockToken({
id: mockUser.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
// act
const response = await app?.inject({
method: 'DELETE',
url: `/v1/users/${mockUser.id}`,
headers: {
authorization: `Bearer ${mockUserToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
})
it('Allows platform owner to delete user', async () => {
// arrange
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetupWithApiKey()
const { mockUser } = await mockBasicUser({
user: {
platformId: mockPlatform.id,
platformRole: PlatformRole.MEMBER,
},
})
const mockOwnerToken = await generateMockToken({
id: mockOwner.id,
type: PrincipalType.USER,
projectId: mockProject.id,
platform: {
id: mockPlatform.id,
},
})
// act
const response = await app?.inject({
method: 'DELETE',
url: `/v1/users/${mockUser.id}`,
headers: {
authorization: `Bearer ${mockOwnerToken}`,
},
})
// assert
expect(response?.statusCode).toBe(StatusCodes.NO_CONTENT)
})
})
})

View File

@@ -0,0 +1,102 @@
import { faker } from '@faker-js/faker'
import { FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
import {
createMockCustomDomain,
mockAndSaveBasicSetup,
} from '../../../../test/helpers/mocks'
import { createMockSignUpRequest } from '../../../helpers/mocks/authn'
let app: FastifyInstance | null = null
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('Authentication API', () => {
describe('Sign up Endpoint', () => {
it('Adds new user', async () => {
// arrange
const mockSignUpRequest = createMockSignUpRequest()
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/authentication/sign-up',
body: mockSignUpRequest,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
const responseBody = response?.json()
expect(responseBody?.id).toHaveLength(21)
expect(responseBody?.created).toBeDefined()
expect(responseBody?.updated).toBeDefined()
expect(responseBody?.email).toBe(mockSignUpRequest.email.toLocaleLowerCase().trim())
expect(responseBody?.firstName).toBe(mockSignUpRequest.firstName)
expect(responseBody?.lastName).toBe(mockSignUpRequest.lastName)
expect(responseBody?.trackEvents).toBe(mockSignUpRequest.trackEvents)
expect(responseBody?.newsLetter).toBe(mockSignUpRequest.newsLetter)
expect(responseBody?.password).toBeUndefined()
expect(responseBody?.status).toBe('ACTIVE')
expect(responseBody?.verified).toBe(true)
expect(responseBody?.platformId).toBeDefined()
expect(responseBody?.externalId).toBe(null)
expect(responseBody?.projectId).toHaveLength(21)
expect(responseBody?.token).toBeDefined()
})
})
it('fails to sign up invited user platform if no project exist', async () => {
// arrange
const { mockPlatform } = await mockAndSaveBasicSetup({
platform: {
emailAuthEnabled: true,
enforceAllowedAuthDomains: false,
},
plan: {
ssoEnabled: false,
},
})
const mockCustomDomain = createMockCustomDomain({
platformId: mockPlatform.id,
})
await databaseConnection()
.getRepository('custom_domain')
.save(mockCustomDomain)
const mockedUpEmail = faker.internet.email()
const mockSignUpRequest = createMockSignUpRequest({ email: mockedUpEmail })
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/authentication/sign-up',
headers: {
Host: mockCustomDomain.domain,
},
body: mockSignUpRequest,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
const responseBody = response?.json()
expect(responseBody?.code).toBe('INVITATION_ONLY_SIGN_UP')
})
})

View File

@@ -0,0 +1,241 @@
import { faker } from '@faker-js/faker'
import { nanoid } from 'nanoid'
import { projectDiffService } from '../../../../../../../../src/app/ee/projects/project-release/project-state/project-diff.service'
import { projectStateService } from '../../../../../../../../src/app/ee/projects/project-release/project-state/project-state.service'
import { system } from '../../../../../../../../src/app/helper/system/system'
import { flowGenerator } from '../../../../../../../helpers/flow-generator'
const logger = system.globalLogger()
describe('Flow Diff Service', () => {
it('should return the flow to delete', async () => {
const flowTwo = flowGenerator.simpleActionAndTrigger()
const diff = await projectDiffService.diff({
currentState: {
flows: [flowTwo],
},
newState: {
flows: [],
},
})
expect(diff.flows.length).toBe(1)
expect(diff.flows[0].type).toBe('DELETE_FLOW')
expect(diff.flows[0].flowState).toBe(flowTwo)
})
it('should return the flow to create', async () => {
const flowTwo = flowGenerator.simpleActionAndTrigger()
const diff = await projectDiffService.diff({
currentState: {
flows: [],
},
newState: {
flows: [flowTwo],
},
})
expect(diff.flows.length).toBe(1)
expect(diff.flows[0].type).toBe('CREATE_FLOW')
expect(diff.flows[0].flowState).toBe(flowTwo)
})
it('should return the flow to create If the mapping is invalid', async () => {
const flowOne = flowGenerator.simpleActionAndTrigger(nanoid())
const flowTwo = flowGenerator.simpleActionAndTrigger()
const diff = await projectDiffService.diff({
currentState: {
flows: [flowTwo],
},
newState: {
flows: [flowOne],
},
})
expect(diff.flows).toEqual([
{
type: 'DELETE_FLOW',
flowState: flowTwo,
},
{
type: 'CREATE_FLOW',
flowState: flowOne,
},
])
})
it('should return the flow to update', async () => {
const flowTwo = flowGenerator.simpleActionAndTrigger()
const flowOne = flowGenerator.simpleActionAndTrigger(flowTwo.id)
const diff = await projectDiffService.diff({
currentState: {
flows: [flowOne],
},
newState: {
flows: [flowTwo],
},
})
expect(diff.flows.length).toBe(1)
expect(diff.flows[0]).toEqual({
type: 'UPDATE_FLOW',
flowState: flowOne,
newFlowState: flowTwo,
})
})
it('should skip the flow to update if the flow is not changed', async () => {
const flowOne = flowGenerator.simpleActionAndTrigger()
const flowOneDist = flowGenerator.randomizeMetadata(undefined, flowOne.version)
flowOneDist.version.trigger.settings.propertySettings = faker.airline.airplane()
flowOne.externalId = flowOneDist.id
const stateOne = projectStateService(logger).getFlowState(flowOne)
const stateTwo = projectStateService(logger).getFlowState(flowOneDist)
const diff = await projectDiffService.diff({
currentState: {
flows: [stateOne],
},
newState: {
flows: [stateTwo],
},
})
expect(diff.flows).toEqual([])
})
it('should return the flow to create, update and delete', async () => {
const flowOne = flowGenerator.simpleActionAndTrigger()
const flowTwo = flowGenerator.simpleActionAndTrigger()
const flowThree = flowGenerator.simpleActionAndTrigger()
const flowOneDist = flowGenerator.simpleActionAndTrigger()
flowOne.externalId = flowOneDist.id
const diff = await projectDiffService.diff({
currentState: {
flows: [flowOne, flowThree],
},
newState: {
flows: [flowOneDist, flowTwo],
},
})
expect(diff.flows.length).toBe(3)
expect(diff.flows).toEqual([
{
type: 'DELETE_FLOW',
flowState: flowThree,
},
{
type: 'CREATE_FLOW',
flowState: flowTwo,
},
{
type: 'UPDATE_FLOW',
flowState: flowOne,
newFlowState: flowOneDist,
},
])
})
it('should compare piece version only based on major and minor version', async () => {
const flowOne = flowGenerator.simpleActionAndTrigger()
const flowTwo = JSON.parse(JSON.stringify(flowOne))
flowTwo.version.trigger.settings.pieceVersion = '0.1.1'
flowOne.version.trigger.settings.pieceVersion = '0.1.0'
const diff = await projectDiffService.diff({
currentState: {
flows: [flowOne],
},
newState: {
flows: [flowTwo],
},
})
expect(diff.flows).toEqual([])
})
it('should detect major piece version change', async () => {
const flowOne = flowGenerator.simpleActionAndTrigger()
const flowTwo = JSON.parse(JSON.stringify(flowOne))
flowTwo.version.trigger.settings.pieceVersion = '0.2.1'
flowOne.version.trigger.settings.pieceVersion = '0.1.0'
const diff = await projectDiffService.diff({
currentState: {
flows: [flowOne],
},
newState: {
flows: [flowTwo],
},
})
expect(diff.flows).toEqual([
{
type: 'UPDATE_FLOW',
flowState: flowOne,
newFlowState: flowTwo,
},
])
})
it('should not detect flow as changed when trigger properties are in different order', async () => {
const flowOne = flowGenerator.simpleActionAndTrigger()
const flowStateOne = projectStateService(logger).getFlowState(flowOne)
// Create a flow with identical trigger content but different property ordering
const flowTwo = {
...flowOne,
version: {
...flowOne.version,
trigger: {
// Reorder trigger properties but keep same content
settings: flowOne.version.trigger.settings, // settings first
valid: flowOne.version.trigger.valid, // valid second
type: flowOne.version.trigger.type, // type third
name: flowOne.version.trigger.name, // name fourth
displayName: flowOne.version.trigger.displayName, // displayName last
nextAction: flowOne.version.trigger.nextAction,
},
},
}
const flowStateTwo = projectStateService(logger).getFlowState(flowTwo)
// Also test with nested trigger.settings properties in different order
const flowThree = {
...flowOne,
version: {
...flowOne.version,
trigger: {
...flowOne.version.trigger,
settings: {
// Reorder settings properties but keep same content
propertySettings: flowOne.version.trigger.settings.propertySettings, // propertySettings first
input: flowOne.version.trigger.settings.input, // input second
triggerName: flowOne.version.trigger.settings.triggerName, // triggerName third
pieceVersion: flowOne.version.trigger.settings.pieceVersion, // pieceVersion fourth
pieceName: flowOne.version.trigger.settings.pieceName, // pieceName last
},
},
},
}
const flowStateThree = projectStateService(logger).getFlowState(flowThree)
// Test flowOne vs flowTwo (different trigger top-level property order)
const diff1 = await projectDiffService.diff({
currentState: {
flows: [flowStateOne],
},
newState: {
flows: [flowStateTwo],
},
})
// Test flowOne vs flowThree (different trigger.settings property order)
const diff2 = await projectDiffService.diff({
currentState: {
flows: [flowStateOne],
},
newState: {
flows: [flowStateThree],
},
})
// Both should detect no changes despite different property ordering
expect(diff1.flows).toEqual([])
expect(diff2.flows).toEqual([])
})
})

View File

@@ -0,0 +1,306 @@
import { nanoid } from 'nanoid'
import { projectDiffService } from '../../../../../../../../src/app/ee/projects/project-release/project-state/project-diff.service'
import { projectStateService } from '../../../../../../../../src/app/ee/projects/project-release/project-state/project-state.service'
import { system } from '../../../../../../../../src/app/helper/system/system'
import { tableGenerator } from '../../../../../../../helpers/table-generator'
describe('Table Diff Service', () => {
it('should return the table to delete', async () => {
const tableTwo = tableGenerator.simpleTable({})
const diff = await projectDiffService.diff({
currentState: {
flows: [],
tables: [tableTwo],
},
newState: {
flows: [],
tables: [],
},
})
expect(diff.tables.length).toBe(1)
expect(diff.tables[0].type).toBe('DELETE_TABLE')
expect(diff.tables[0].tableState.externalId).toBe(tableTwo.externalId)
})
it('should return the table to create', async () => {
const tableTwo = tableGenerator.simpleTable({})
const diff = await projectDiffService.diff({
currentState: {
flows: [],
tables: [],
},
newState: {
flows: [],
tables: [tableTwo],
},
})
expect(diff.tables.length).toBe(1)
expect(diff.tables[0].type).toBe('CREATE_TABLE')
expect(diff.tables[0].tableState).toBe(tableTwo)
})
it('should return the table to create if the mapping is invalid', async () => {
const tableOne = tableGenerator.simpleTable({ externalId: nanoid() })
const tableTwo = tableGenerator.simpleTable({})
const diff = await projectDiffService.diff({
currentState: {
flows: [],
tables: [tableTwo],
},
newState: {
flows: [],
tables: [tableOne],
},
})
const sortedTables = [...diff.tables].sort((a, b) => a.type.localeCompare(b.type))
expect(sortedTables).toEqual([
{
type: 'CREATE_TABLE',
tableState: tableOne,
},
{
type: 'DELETE_TABLE',
tableState: tableTwo,
},
])
})
it('should return the table to update', async () => {
const tableTwo = tableGenerator.simpleTable({})
const tableOne = tableGenerator.simpleTable({ externalId: tableTwo.externalId })
tableOne.name = 'Updated Table Name'
const diff = await projectDiffService.diff({
currentState: {
flows: [],
tables: [tableOne],
},
newState: {
flows: [],
tables: [tableTwo],
},
})
expect(diff.tables.length).toBe(1)
expect(diff.tables[0]).toEqual({
type: 'UPDATE_TABLE',
tableState: tableOne,
newTableState: tableTwo,
})
})
it('should skip the table to update if the table is not changed', async () => {
const tableOne = tableGenerator.simpleTable({})
const tableOneDist = tableGenerator.simpleTable({ externalId: tableOne.externalId })
tableOneDist.name = tableOne.name
const diff = await projectDiffService.diff({
currentState: {
flows: [],
tables: [tableOne],
},
newState: {
flows: [],
tables: [tableOneDist],
},
})
expect(diff.tables).toEqual([
{
type: 'UPDATE_TABLE',
tableState: tableOne,
newTableState: tableOneDist,
},
])
})
it('should return the table to create, update and delete', async () => {
const tableOne = tableGenerator.simpleTable({})
const tableTwo = tableGenerator.simpleTable({})
const tableThree = tableGenerator.simpleTable({})
const tableOneDist = tableGenerator.simpleTable({ externalId: tableOne.externalId })
tableOneDist.name = 'Updated Table One'
const diff = await projectDiffService.diff({
currentState: {
flows: [],
tables: [tableOne, tableThree],
},
newState: {
flows: [],
tables: [tableOneDist, tableTwo],
},
})
expect(diff.tables.length).toBe(3)
expect(diff.tables).toEqual(
expect.arrayContaining([
{
type: 'DELETE_TABLE',
tableState: expect.objectContaining({
externalId: tableThree.externalId,
}),
},
{
type: 'CREATE_TABLE',
tableState: expect.objectContaining({
externalId: tableTwo.externalId,
name: tableTwo.name,
fields: tableTwo.fields,
id: tableTwo.id,
}),
},
{
type: 'UPDATE_TABLE',
tableState: expect.objectContaining({
externalId: tableOne.externalId,
name: tableOne.name,
fields: tableOne.fields,
id: tableOne.id,
}),
newTableState: expect.objectContaining({
externalId: tableOneDist.externalId,
name: tableOneDist.name,
fields: tableOneDist.fields,
id: tableOneDist.id,
}),
},
]),
)
})
it('should detect field changes in table update', async () => {
const tableOne = tableGenerator.simpleTable({})
const tableOneDist = tableGenerator.simpleTable({ externalId: tableOne.externalId })
tableOneDist.fields.push(tableGenerator.generateRandomField(tableOneDist.id))
const diff = await projectDiffService.diff({
currentState: {
flows: [],
tables: [tableOne],
},
newState: {
flows: [],
tables: [tableOneDist],
},
})
expect(diff.tables.length).toBe(1)
expect(diff.tables[0].type).toBe('UPDATE_TABLE')
expect(diff.tables[0].tableState).toBe(tableOne)
})
it('should detect dropdown field changes', async () => {
const dropdownField = tableGenerator.generateRandomDropdownField()
const tableOne = projectStateService(system.globalLogger()).getTableState(tableGenerator.simpleTable({}))
tableOne.fields.push(dropdownField)
const tableOneDist = {
...tableOne,
fields: [
...tableOne.fields,
{
...dropdownField,
data: {
...dropdownField.data,
options: [
...dropdownField.data!.options,
{ value: 'Pending' },
],
},
},
],
}
const diff = await projectDiffService.diff({
currentState: {
flows: [],
tables: [tableOne],
},
newState: {
flows: [],
tables: [tableOneDist],
},
})
expect(diff.tables.length).toBe(1)
expect(diff.tables[0].type).toBe('UPDATE_TABLE')
})
it('should not detect table as changed when only id field differs', async () => {
const tableOne = tableGenerator.simpleTable({})
const tableTwo = tableGenerator.simpleTable({ externalId: tableOne.externalId })
// Ensure all fields are identical
tableTwo.name = tableOne.name
tableTwo.fields = tableOne.fields
const diff = await projectDiffService.diff({
currentState: {
flows: [],
tables: [tableOne],
},
newState: {
flows: [],
tables: [tableTwo],
},
})
expect(diff.tables.length).toBe(0)
})
it('should not detect table as changed when properties are in different order', async () => {
const tableOne = tableGenerator.simpleTable({})
// Create table with same content but different property ordering
// This tests that deepEqual correctly handles property order independence
const tableTwo = {
fields: tableOne.fields, // fields first
externalId: tableOne.externalId, // externalId second
name: tableOne.name, // name third
id: tableOne.id, // id last
}
// Also test with fields in different order but same content
const tableThree = {
...tableOne,
fields: [
{
externalId: tableOne.fields[0].externalId, // externalId first
type: tableOne.fields[0].type, // type second
name: tableOne.fields[0].name, // name last
},
{
type: tableOne.fields[1].type, // type first
name: tableOne.fields[1].name, // name second
externalId: tableOne.fields[1].externalId, // externalId last
},
],
}
// Test tableOne vs tableTwo (different top-level property order)
const diff1 = await projectDiffService.diff({
currentState: {
flows: [],
tables: [tableOne],
},
newState: {
flows: [],
tables: [tableTwo],
},
})
// Test tableOne vs tableThree (different field property order)
const diff2 = await projectDiffService.diff({
currentState: {
flows: [],
tables: [tableOne],
},
newState: {
flows: [],
tables: [tableThree],
},
})
// Both should detect no changes despite different property ordering
// This validates that deepEqual is working correctly for property order independence
expect(diff1.tables.length).toBe(0)
expect(diff2.tables.length).toBe(0)
})
})

View File

@@ -0,0 +1,693 @@
import { DiffState, FlowProjectOperationType, FlowStatus, FlowSyncError } from '@activepieces/shared'
import { nanoid } from 'nanoid'
import { projectStateHelper } from '../../../../../../../src/app/ee/projects/project-release/project-state/project-state-helper'
import { projectStateService } from '../../../../../../../src/app/ee/projects/project-release/project-state/project-state.service'
import { system } from '../../../../../../../src/app/helper/system/system'
import { flowGenerator } from '../../../../../../helpers/flow-generator'
// Mock the project state helper
jest.mock('../../../../../../../src/app/ee/projects/project-release/project-state/project-state-helper')
const mockProjectStateHelper = projectStateHelper as jest.MockedFunction<typeof projectStateHelper>
const logger = system.globalLogger()
describe('ProjectStateService.apply - Flow Operations', () => {
let mockCreateFlowInProject: jest.Mock
let mockUpdateFlowInProject: jest.Mock
let mockDeleteFlowFromProject: jest.Mock
let mockRepublishFlow: jest.Mock
beforeEach(() => {
jest.clearAllMocks()
// Set up mocks for all helper methods
mockCreateFlowInProject = jest.fn()
mockUpdateFlowInProject = jest.fn()
mockDeleteFlowFromProject = jest.fn()
mockRepublishFlow = jest.fn()
mockProjectStateHelper.mockReturnValue({
createFlowInProject: mockCreateFlowInProject,
updateFlowInProject: mockUpdateFlowInProject,
deleteFlowFromProject: mockDeleteFlowFromProject,
republishFlow: mockRepublishFlow,
})
})
describe('CREATE_FLOW operation', () => {
it('should create a flow and republish it with default enabled status', async () => {
// Arrange
const projectId = nanoid()
const platformId = nanoid()
const flowState = flowGenerator.simpleActionAndTrigger()
const createdFlow = { ...flowState, id: nanoid() }
mockCreateFlowInProject.mockResolvedValue(createdFlow)
mockRepublishFlow.mockResolvedValue(null)
const diffs = {
flows: [{
type: FlowProjectOperationType.CREATE_FLOW as const,
flowState,
}],
connections: [],
tables: [],
agents: [],
}
// Act
await projectStateService(logger).apply({
projectId,
diffs,
platformId,
log: logger,
})
// Assert
expect(mockCreateFlowInProject).toHaveBeenCalledTimes(1)
expect(mockCreateFlowInProject).toHaveBeenCalledWith(flowState, projectId)
expect(mockRepublishFlow).toHaveBeenCalledTimes(1)
expect(mockRepublishFlow).toHaveBeenCalledWith({
flow: createdFlow,
projectId,
// Note: status should be undefined (default to enabled)
})
})
it('should handle multiple create flow operations', async () => {
// Arrange
const projectId = nanoid()
const platformId = nanoid()
const flowState1 = flowGenerator.simpleActionAndTrigger()
const flowState2 = flowGenerator.simpleActionAndTrigger()
const createdFlow1 = { ...flowState1, id: nanoid() }
const createdFlow2 = { ...flowState2, id: nanoid() }
mockCreateFlowInProject
.mockResolvedValueOnce(createdFlow1)
.mockResolvedValueOnce(createdFlow2)
mockRepublishFlow.mockResolvedValue(null)
const diffs = {
flows: [
{
type: FlowProjectOperationType.CREATE_FLOW as const,
flowState: flowState1,
},
{
type: FlowProjectOperationType.CREATE_FLOW as const,
flowState: flowState2,
},
],
connections: [],
tables: [],
agents: [],
}
// Act
await projectStateService(logger).apply({
projectId,
diffs,
platformId,
log: logger,
})
// Assert
expect(mockCreateFlowInProject).toHaveBeenCalledTimes(2)
expect(mockRepublishFlow).toHaveBeenCalledTimes(2)
expect(mockCreateFlowInProject).toHaveBeenNthCalledWith(1, flowState1, projectId)
expect(mockCreateFlowInProject).toHaveBeenNthCalledWith(2, flowState2, projectId)
})
it('should propagate errors from createFlowInProject', async () => {
// Arrange
const projectId = nanoid()
const platformId = nanoid()
const flowState = flowGenerator.simpleActionAndTrigger()
const expectedError = new Error('Failed to create flow')
mockCreateFlowInProject.mockRejectedValue(expectedError)
const diffs = {
flows: [{
type: FlowProjectOperationType.CREATE_FLOW as const,
flowState,
}],
connections: [],
tables: [],
agents: [],
}
// Act & Assert
await expect(projectStateService(logger).apply({
projectId,
diffs,
platformId,
log: logger,
})).rejects.toThrow('Failed to create flow')
expect(mockCreateFlowInProject).toHaveBeenCalledTimes(1)
expect(mockRepublishFlow).not.toHaveBeenCalled()
})
})
describe('UPDATE_FLOW operation', () => {
it('should update a flow and republish it with preserved status', async () => {
// Arrange
const projectId = nanoid()
const platformId = nanoid()
const originalFlowState = flowGenerator.simpleActionAndTrigger()
originalFlowState.status = FlowStatus.DISABLED
const newFlowState = { ...originalFlowState }
newFlowState.version.displayName = 'Updated Flow Name'
const updatedFlow = { ...newFlowState, id: nanoid() }
mockUpdateFlowInProject.mockResolvedValue(updatedFlow)
mockRepublishFlow.mockResolvedValue(null)
const diffs = {
flows: [{
type: FlowProjectOperationType.UPDATE_FLOW as const,
flowState: originalFlowState,
newFlowState,
}],
connections: [],
tables: [],
agents: [],
}
// Act
await projectStateService(logger).apply({
projectId,
diffs,
platformId,
log: logger,
})
// Assert
expect(mockUpdateFlowInProject).toHaveBeenCalledTimes(1)
expect(mockUpdateFlowInProject).toHaveBeenCalledWith(originalFlowState, newFlowState, projectId)
expect(mockRepublishFlow).toHaveBeenCalledTimes(1)
expect(mockRepublishFlow).toHaveBeenCalledWith({
flow: updatedFlow,
projectId,
status: FlowStatus.DISABLED, // Should preserve original status
})
})
it('should handle enabled status preservation', async () => {
// Arrange
const projectId = nanoid()
const platformId = nanoid()
const originalFlowState = flowGenerator.simpleActionAndTrigger()
originalFlowState.status = FlowStatus.ENABLED
const newFlowState = { ...originalFlowState }
const updatedFlow = { ...newFlowState, id: nanoid() }
mockUpdateFlowInProject.mockResolvedValue(updatedFlow)
mockRepublishFlow.mockResolvedValue(null)
const diffs = {
flows: [{
type: FlowProjectOperationType.UPDATE_FLOW as const,
flowState: originalFlowState,
newFlowState,
}],
connections: [],
tables: [],
agents: [],
}
// Act
await projectStateService(logger).apply({
projectId,
diffs,
platformId,
log: logger,
})
// Assert
expect(mockRepublishFlow).toHaveBeenCalledWith({
flow: updatedFlow,
projectId,
status: FlowStatus.ENABLED,
})
})
it('should handle multiple update operations', async () => {
// Arrange
const projectId = nanoid()
const platformId = nanoid()
const originalFlow1 = flowGenerator.simpleActionAndTrigger()
const originalFlow2 = flowGenerator.simpleActionAndTrigger()
originalFlow1.status = FlowStatus.ENABLED
originalFlow2.status = FlowStatus.DISABLED
const newFlow1 = { ...originalFlow1 }
const newFlow2 = { ...originalFlow2 }
const updatedFlow1 = { ...newFlow1, id: nanoid() }
const updatedFlow2 = { ...newFlow2, id: nanoid() }
mockUpdateFlowInProject
.mockResolvedValueOnce(updatedFlow1)
.mockResolvedValueOnce(updatedFlow2)
mockRepublishFlow.mockResolvedValue(null)
const diffs = {
flows: [
{
type: FlowProjectOperationType.UPDATE_FLOW as const,
flowState: originalFlow1,
newFlowState: newFlow1,
},
{
type: FlowProjectOperationType.UPDATE_FLOW as const,
flowState: originalFlow2,
newFlowState: newFlow2,
},
],
connections: [],
tables: [],
agents: [],
}
// Act
await projectStateService(logger).apply({
projectId,
diffs,
platformId,
log: logger,
})
// Assert
expect(mockUpdateFlowInProject).toHaveBeenCalledTimes(2)
expect(mockRepublishFlow).toHaveBeenCalledTimes(2)
expect(mockRepublishFlow).toHaveBeenNthCalledWith(1, {
flow: updatedFlow1,
projectId,
status: FlowStatus.ENABLED,
})
expect(mockRepublishFlow).toHaveBeenNthCalledWith(2, {
flow: updatedFlow2,
projectId,
status: FlowStatus.DISABLED,
})
})
it('should propagate errors from updateFlowInProject', async () => {
// Arrange
const projectId = nanoid()
const platformId = nanoid()
const originalFlowState = flowGenerator.simpleActionAndTrigger()
const newFlowState = { ...originalFlowState }
const expectedError = new Error('Failed to update flow')
mockUpdateFlowInProject.mockRejectedValue(expectedError)
const diffs = {
flows: [{
type: FlowProjectOperationType.UPDATE_FLOW as const,
flowState: originalFlowState,
newFlowState,
}],
connections: [],
tables: [],
agents: [],
}
// Act & Assert
await expect(projectStateService(logger).apply({
projectId,
diffs,
platformId,
log: logger,
})).rejects.toThrow('Failed to update flow')
expect(mockUpdateFlowInProject).toHaveBeenCalledTimes(1)
expect(mockRepublishFlow).not.toHaveBeenCalled()
})
})
describe('DELETE_FLOW operation', () => {
it('should delete a flow', async () => {
// Arrange
const projectId = nanoid()
const platformId = nanoid()
const flowState = flowGenerator.simpleActionAndTrigger()
mockDeleteFlowFromProject.mockResolvedValue(undefined)
const diffs = {
flows: [{
type: FlowProjectOperationType.DELETE_FLOW as const,
flowState,
}],
connections: [],
tables: [],
agents: [],
}
// Act
await projectStateService(logger).apply({
projectId,
diffs,
platformId,
log: logger,
})
// Assert
expect(mockDeleteFlowFromProject).toHaveBeenCalledTimes(1)
expect(mockDeleteFlowFromProject).toHaveBeenCalledWith(flowState.id, projectId)
// Delete operations don't trigger republish
expect(mockRepublishFlow).not.toHaveBeenCalled()
})
it('should handle multiple delete operations', async () => {
// Arrange
const projectId = nanoid()
const platformId = nanoid()
const flowState1 = flowGenerator.simpleActionAndTrigger()
const flowState2 = flowGenerator.simpleActionAndTrigger()
mockDeleteFlowFromProject.mockResolvedValue(undefined)
const diffs = {
flows: [
{
type: FlowProjectOperationType.DELETE_FLOW as const,
flowState: flowState1,
},
{
type: FlowProjectOperationType.DELETE_FLOW as const,
flowState: flowState2,
},
],
connections: [],
tables: [],
agents: [],
}
// Act
await projectStateService(logger).apply({
projectId,
diffs,
platformId,
log: logger,
})
// Assert
expect(mockDeleteFlowFromProject).toHaveBeenCalledTimes(2)
expect(mockDeleteFlowFromProject).toHaveBeenNthCalledWith(1, flowState1.id, projectId)
expect(mockDeleteFlowFromProject).toHaveBeenNthCalledWith(2, flowState2.id, projectId)
expect(mockRepublishFlow).not.toHaveBeenCalled()
})
it('should propagate errors from deleteFlowFromProject', async () => {
// Arrange
const projectId = nanoid()
const platformId = nanoid()
const flowState = flowGenerator.simpleActionAndTrigger()
const expectedError = new Error('Failed to delete flow')
mockDeleteFlowFromProject.mockRejectedValue(expectedError)
const diffs = {
flows: [{
type: FlowProjectOperationType.DELETE_FLOW as const,
flowState,
}],
connections: [],
tables: [],
agents: [],
}
// Act & Assert
await expect(projectStateService(logger).apply({
projectId,
diffs,
platformId,
log: logger,
})).rejects.toThrow('Failed to delete flow')
expect(mockDeleteFlowFromProject).toHaveBeenCalledTimes(1)
})
})
describe('Republish error handling', () => {
it('should continue processing even if republish returns an error', async () => {
// Arrange
const projectId = nanoid()
const platformId = nanoid()
const flowState = flowGenerator.simpleActionAndTrigger()
const createdFlow = { ...flowState, id: nanoid() }
const syncError: FlowSyncError = {
flowId: createdFlow.id,
message: 'Flow is not valid',
}
mockCreateFlowInProject.mockResolvedValue(createdFlow)
mockRepublishFlow.mockResolvedValue(syncError)
const diffs = {
flows: [{
type: FlowProjectOperationType.CREATE_FLOW as const,
flowState,
}],
connections: [],
tables: [],
agents: [],
}
// Act - Should not throw despite republish error
await expect(projectStateService(logger).apply({
projectId,
diffs,
platformId,
log: logger,
})).resolves.not.toThrow()
// Assert
expect(mockCreateFlowInProject).toHaveBeenCalledTimes(1)
expect(mockRepublishFlow).toHaveBeenCalledTimes(1)
})
it('should handle republish errors for update operations', async () => {
// Arrange
const projectId = nanoid()
const platformId = nanoid()
const originalFlowState = flowGenerator.simpleActionAndTrigger()
const newFlowState = { ...originalFlowState }
const updatedFlow = { ...newFlowState, id: nanoid() }
const syncError: FlowSyncError = {
flowId: updatedFlow.id,
message: 'Failed to publish flow',
}
mockUpdateFlowInProject.mockResolvedValue(updatedFlow)
mockRepublishFlow.mockResolvedValue(syncError)
const diffs = {
flows: [{
type: FlowProjectOperationType.UPDATE_FLOW as const,
flowState: originalFlowState,
newFlowState,
}],
connections: [],
tables: [],
agents: [],
}
// Act
await projectStateService(logger).apply({
projectId,
diffs,
platformId,
log: logger,
})
// Assert
expect(mockUpdateFlowInProject).toHaveBeenCalledTimes(1)
expect(mockRepublishFlow).toHaveBeenCalledTimes(1)
})
})
describe('Mixed flow operations', () => {
it('should handle create, update, and delete operations in one apply call', async () => {
// Arrange
const projectId = nanoid()
const platformId = nanoid()
// Create operation
const createFlowState = flowGenerator.simpleActionAndTrigger()
const createdFlow = { ...createFlowState, id: nanoid() }
// Update operation
const originalFlowState = flowGenerator.simpleActionAndTrigger()
originalFlowState.status = FlowStatus.ENABLED
const newFlowState = { ...originalFlowState }
newFlowState.version.displayName = 'Updated Flow'
const updatedFlow = { ...newFlowState, id: nanoid() }
// Delete operation
const deleteFlowState = flowGenerator.simpleActionAndTrigger()
mockCreateFlowInProject.mockResolvedValue(createdFlow)
mockUpdateFlowInProject.mockResolvedValue(updatedFlow)
mockDeleteFlowFromProject.mockResolvedValue(undefined)
mockRepublishFlow.mockResolvedValue(null)
const diffs = {
flows: [
{
type: FlowProjectOperationType.CREATE_FLOW as const,
flowState: createFlowState,
},
{
type: FlowProjectOperationType.UPDATE_FLOW as const,
flowState: originalFlowState,
newFlowState,
},
{
type: FlowProjectOperationType.DELETE_FLOW as const,
flowState: deleteFlowState,
},
],
connections: [],
tables: [],
agents: [],
}
// Act
await projectStateService(logger).apply({
projectId,
diffs,
platformId,
log: logger,
})
// Assert
expect(mockCreateFlowInProject).toHaveBeenCalledTimes(1)
expect(mockUpdateFlowInProject).toHaveBeenCalledTimes(1)
expect(mockDeleteFlowFromProject).toHaveBeenCalledTimes(1)
expect(mockRepublishFlow).toHaveBeenCalledTimes(2) // Only create and update trigger republish
expect(mockCreateFlowInProject).toHaveBeenCalledWith(createFlowState, projectId)
expect(mockUpdateFlowInProject).toHaveBeenCalledWith(originalFlowState, newFlowState, projectId)
expect(mockDeleteFlowFromProject).toHaveBeenCalledWith(deleteFlowState.id, projectId)
})
it('should process operations sequentially and handle partial failures', async () => {
// Arrange
const projectId = nanoid()
const platformId = nanoid()
const createFlowState = flowGenerator.simpleActionAndTrigger()
const updateFlowState = flowGenerator.simpleActionAndTrigger()
const newFlowState = { ...updateFlowState }
const createdFlow = { ...createFlowState, id: nanoid() }
// First operation succeeds, second fails
mockCreateFlowInProject.mockResolvedValue(createdFlow)
mockUpdateFlowInProject.mockRejectedValue(new Error('Update failed'))
mockRepublishFlow.mockResolvedValue(null)
const diffs = {
flows: [
{
type: FlowProjectOperationType.CREATE_FLOW as const,
flowState: createFlowState,
},
{
type: FlowProjectOperationType.UPDATE_FLOW as const,
flowState: updateFlowState,
newFlowState,
},
],
connections: [],
tables: [],
agents: [],
}
// Act & Assert
await expect(projectStateService(logger).apply({
projectId,
diffs,
platformId,
log: logger,
})).rejects.toThrow('Update failed')
// First operation should have been executed
expect(mockCreateFlowInProject).toHaveBeenCalledTimes(1)
expect(mockRepublishFlow).toHaveBeenCalledTimes(1)
// Second operation should have been attempted
expect(mockUpdateFlowInProject).toHaveBeenCalledTimes(1)
})
})
describe('Edge cases and validation', () => {
it('should handle empty flow operations array', async () => {
// Arrange
const projectId = nanoid()
const platformId = nanoid()
const diffs = {
flows: [],
connections: [],
tables: [],
agents: [],
}
// Act
await projectStateService(logger).apply({
projectId,
diffs,
platformId,
log: logger,
})
// Assert
expect(mockCreateFlowInProject).not.toHaveBeenCalled()
expect(mockUpdateFlowInProject).not.toHaveBeenCalled()
expect(mockDeleteFlowFromProject).not.toHaveBeenCalled()
expect(mockRepublishFlow).not.toHaveBeenCalled()
})
it('should handle unknown operation type gracefully', async () => {
// Arrange
const projectId = nanoid()
const platformId = nanoid()
const flowState = flowGenerator.simpleActionAndTrigger()
const diffs = {
flows: [
{
type: 'UNKNOWN_OPERATION',
flowState,
},
],
connections: [],
tables: [],
agents: [],
} as unknown as DiffState
// Act - Should not throw, just skip unknown operations
await projectStateService(logger).apply({
projectId,
diffs,
platformId,
log: logger,
})
// Assert
expect(mockCreateFlowInProject).not.toHaveBeenCalled()
expect(mockUpdateFlowInProject).not.toHaveBeenCalled()
expect(mockDeleteFlowFromProject).not.toHaveBeenCalled()
expect(mockRepublishFlow).not.toHaveBeenCalled()
})
})
})

View File

@@ -0,0 +1,32 @@
import { PopulatedFlow } from '@activepieces/shared'
import { projectStateService } from '../../../../../../../src/app/ee/projects/project-release/project-state/project-state.service'
import { system } from '../../../../../../../src/app/helper/system/system'
import { flowGenerator } from '../../../../../../helpers/flow-generator'
import { tableGenerator } from '../../../../../../helpers/table-generator'
const logger = system.globalLogger()
describe('ProjectStateService', () => {
describe('getFlowState', () => {
it('should remove extra properties from flow state', () => {
const flow: PopulatedFlow = {
...flowGenerator.simpleActionAndTrigger(),
extraProperty: 'should be removed',
} as PopulatedFlow
const flowState = projectStateService(logger).getFlowState(flow)
expect(flowState).not.toHaveProperty('extraProperty')
})
})
describe('getTableState', () => {
it('should remove extra properties from table state', () => {
const table = {
...tableGenerator.simpleTable({}),
extraProperty: 'should be removed',
}
const tableState = projectStateService(logger).getTableState(table)
expect(tableState).not.toHaveProperty('extraProperty')
})
})
})