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:
139
activepieces-fork/packages/server/api/test/helpers/auth.ts
Normal file
139
activepieces-fork/packages/server/api/test/helpers/auth.ts
Normal 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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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() },
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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}`,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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',
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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}`,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
|
||||
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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([])
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
Reference in New Issue
Block a user