Add Activepieces integration for workflow automation

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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