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:
@@ -0,0 +1,142 @@
|
||||
import { PlatformRole, PrincipalType } from '@activepieces/shared'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { FastifyInstance } from 'fastify'
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { initializeDatabase } from '../../../../src/app/database'
|
||||
import { databaseConnection } from '../../../../src/app/database/database-connection'
|
||||
import { setupServer } from '../../../../src/app/server'
|
||||
import { generateMockToken } from '../../../helpers/auth'
|
||||
import {
|
||||
createMockApiKey,
|
||||
mockAndSaveBasicSetup,
|
||||
mockBasicUser,
|
||||
} from '../../../helpers/mocks'
|
||||
|
||||
let app: FastifyInstance | null = null
|
||||
|
||||
beforeAll(async () => {
|
||||
await initializeDatabase({ runMigrations: false })
|
||||
app = await setupServer()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await databaseConnection().destroy()
|
||||
await app?.close()
|
||||
})
|
||||
|
||||
describe('API Key API', () => {
|
||||
describe('Create API Key API', () => {
|
||||
it('should create a new API Key', async () => {
|
||||
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockOwner.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
const mockApiKeyName = faker.lorem.word()
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/api-keys',
|
||||
body: {
|
||||
displayName: mockApiKeyName,
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.CREATED)
|
||||
expect(responseBody.id).toHaveLength(21)
|
||||
expect(responseBody.platformId).toBe(mockPlatform.id)
|
||||
expect(responseBody.hashedValue).toBeUndefined()
|
||||
expect(responseBody.displayName).toBe(mockApiKeyName)
|
||||
expect(responseBody.truncatedValue).toHaveLength(4)
|
||||
expect(responseBody.value).toHaveLength(64)
|
||||
expect(responseBody.value).toContain('sk-')
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('Delete API Key endpoint', () => {
|
||||
it('Fail if non owner', async () => {
|
||||
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
const mockApiKey = createMockApiKey({
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
|
||||
await databaseConnection().getRepository('api_key').save(mockApiKey)
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockUser.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/api-keys/${mockApiKey.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
})
|
||||
|
||||
describe('List API Keys endpoint', () => {
|
||||
it('Filters Signing Keys by platform', async () => {
|
||||
// arrange
|
||||
const { mockOwner: mockUserOne, mockPlatform: mockPlatformOne, mockProject: mockProjectOne } = await mockAndSaveBasicSetup()
|
||||
const { mockPlatform: mockPlatformTwo } = await mockAndSaveBasicSetup()
|
||||
|
||||
|
||||
const mockKeyOne = createMockApiKey({
|
||||
platformId: mockPlatformOne.id,
|
||||
})
|
||||
|
||||
const mockKeyTwo = createMockApiKey({
|
||||
platformId: mockPlatformTwo.id,
|
||||
})
|
||||
|
||||
await databaseConnection()
|
||||
.getRepository('api_key')
|
||||
.save([mockKeyOne, mockKeyTwo])
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockUserOne.id,
|
||||
projectId: mockProjectOne.id,
|
||||
platform: { id: mockPlatformOne.id },
|
||||
})
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/api-keys',
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(responseBody.data).toHaveLength(1)
|
||||
expect(responseBody.data[0].id).toBe(mockKeyOne.id)
|
||||
expect(responseBody.data[0].hashedValue).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,309 @@
|
||||
import {
|
||||
AppConnectionType,
|
||||
DefaultProjectRole,
|
||||
PackageType,
|
||||
PlatformRole,
|
||||
PrincipalType,
|
||||
ProjectRole,
|
||||
UpsertAppConnectionRequestBody,
|
||||
} from '@activepieces/shared'
|
||||
import { FastifyBaseLogger, FastifyInstance } from 'fastify'
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { initializeDatabase } from '../../../../src/app/database'
|
||||
import { databaseConnection } from '../../../../src/app/database/database-connection'
|
||||
import { pieceMetadataService } from '../../../../src/app/pieces/metadata/piece-metadata-service'
|
||||
import { setupServer } from '../../../../src/app/server'
|
||||
import { generateMockToken } from '../../../helpers/auth'
|
||||
import {
|
||||
createMockPieceMetadata,
|
||||
createMockProjectMember,
|
||||
mockAndSaveBasicSetup,
|
||||
mockBasicUser,
|
||||
} from '../../../helpers/mocks'
|
||||
|
||||
let app: FastifyInstance | null = null
|
||||
let mockLog: FastifyBaseLogger
|
||||
|
||||
beforeAll(async () => {
|
||||
await initializeDatabase({ runMigrations: false })
|
||||
app = await setupServer()
|
||||
mockLog = app!.log!
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await databaseConnection().destroy()
|
||||
await app?.close()
|
||||
})
|
||||
|
||||
describe('AppConnection API', () => {
|
||||
describe('Upsert AppConnection endpoint', () => {
|
||||
it('Succeeds with metadata field', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.ADMIN,
|
||||
},
|
||||
})
|
||||
|
||||
const mockPieceMetadata = createMockPieceMetadata({
|
||||
projectId: mockProject.id,
|
||||
platformId: mockPlatform.id,
|
||||
packageType: PackageType.REGISTRY,
|
||||
})
|
||||
await databaseConnection().getRepository('piece_metadata').save([mockPieceMetadata])
|
||||
|
||||
pieceMetadataService(mockLog).getOrThrow = jest.fn().mockResolvedValue(mockPieceMetadata)
|
||||
|
||||
const mockToken = await generateMockToken({
|
||||
id: mockUser.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
const mockUpsertAppConnectionRequest: UpsertAppConnectionRequestBody = {
|
||||
externalId: 'test-app-connection-with-metadata',
|
||||
displayName: 'Test Connection with Metadata',
|
||||
pieceName: mockPieceMetadata.name,
|
||||
projectId: mockProject.id,
|
||||
type: AppConnectionType.SECRET_TEXT,
|
||||
value: {
|
||||
type: AppConnectionType.SECRET_TEXT,
|
||||
secret_text: 'test-secret-text',
|
||||
},
|
||||
metadata: {
|
||||
foo: 'bar',
|
||||
},
|
||||
pieceVersion: mockPieceMetadata.version,
|
||||
}
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/app-connections',
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
body: mockUpsertAppConnectionRequest,
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.CREATED)
|
||||
const responseBody = response?.json()
|
||||
expect(responseBody.metadata).toEqual(mockUpsertAppConnectionRequest.metadata)
|
||||
expect(responseBody.pieceVersion).toEqual(mockPieceMetadata.version)
|
||||
// Verify connection can be updated with new metadata
|
||||
const updateResponse = await app?.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/app-connections/${responseBody.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
body: {
|
||||
displayName: 'Updated Connection Name',
|
||||
metadata: {
|
||||
foo: 'baz',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(updateResponse?.statusCode).toBe(StatusCodes.OK)
|
||||
const updatedResponseBody = updateResponse?.json()
|
||||
expect(updatedResponseBody.metadata).toEqual({
|
||||
foo: 'baz',
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
DefaultProjectRole.ADMIN,
|
||||
DefaultProjectRole.EDITOR,
|
||||
])('Succeeds if user role is %s', async (testRole) => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
|
||||
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: testRole }) as ProjectRole
|
||||
|
||||
const mockProjectMember = createMockProjectMember({
|
||||
userId: mockUser.id,
|
||||
platformId: mockPlatform.id,
|
||||
projectId: mockProject.id,
|
||||
projectRoleId: projectRole.id,
|
||||
})
|
||||
await databaseConnection().getRepository('project_member').save([mockProjectMember])
|
||||
|
||||
const mockPieceMetadata = createMockPieceMetadata({
|
||||
projectId: mockProject.id,
|
||||
platformId: mockPlatform.id,
|
||||
packageType: PackageType.REGISTRY,
|
||||
})
|
||||
await databaseConnection().getRepository('piece_metadata').save([mockPieceMetadata])
|
||||
|
||||
pieceMetadataService(mockLog).getOrThrow = jest.fn().mockResolvedValue(mockPieceMetadata)
|
||||
|
||||
const mockToken = await generateMockToken({
|
||||
id: mockUser.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
const mockUpsertAppConnectionRequest: UpsertAppConnectionRequestBody = {
|
||||
externalId: 'test-app-connection',
|
||||
displayName: 'test-app-connection',
|
||||
pieceName: mockPieceMetadata.name,
|
||||
projectId: mockProject.id,
|
||||
type: AppConnectionType.SECRET_TEXT,
|
||||
value: {
|
||||
type: AppConnectionType.SECRET_TEXT,
|
||||
secret_text: 'test-secret-text',
|
||||
},
|
||||
pieceVersion: mockPieceMetadata.version,
|
||||
}
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/app-connections',
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
body: mockUpsertAppConnectionRequest,
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.CREATED)
|
||||
})
|
||||
|
||||
it('Fails if user role is VIEWER', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
|
||||
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.VIEWER }) as ProjectRole
|
||||
|
||||
const mockProjectMember = createMockProjectMember({
|
||||
userId: mockUser.id,
|
||||
platformId: mockPlatform.id,
|
||||
projectId: mockProject.id,
|
||||
projectRoleId: projectRole.id,
|
||||
})
|
||||
await databaseConnection().getRepository('project_member').save([mockProjectMember])
|
||||
|
||||
const mockPieceMetadata = createMockPieceMetadata({
|
||||
projectId: mockProject.id,
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
await databaseConnection().getRepository('piece_metadata').save([mockPieceMetadata])
|
||||
|
||||
pieceMetadataService(mockLog).getOrThrow = jest.fn().mockResolvedValue(mockPieceMetadata)
|
||||
|
||||
const mockToken = await generateMockToken({
|
||||
id: mockUser.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
const mockUpsertAppConnectionRequest: UpsertAppConnectionRequestBody = {
|
||||
externalId: 'test-app-connection',
|
||||
displayName: 'test-app-connection',
|
||||
pieceName: mockPieceMetadata.name,
|
||||
projectId: mockProject.id,
|
||||
type: AppConnectionType.SECRET_TEXT,
|
||||
value: {
|
||||
type: AppConnectionType.SECRET_TEXT,
|
||||
secret_text: 'test-secret-text',
|
||||
},
|
||||
pieceVersion: mockPieceMetadata.version,
|
||||
}
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/app-connections',
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
body: mockUpsertAppConnectionRequest,
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
|
||||
const responseBody = response?.json()
|
||||
expect(responseBody?.code).toBe('PERMISSION_DENIED')
|
||||
expect(responseBody?.params?.userId).toBe(mockUser.id)
|
||||
expect(responseBody?.params?.projectId).toBe(mockProject.id)
|
||||
})
|
||||
})
|
||||
|
||||
describe('List AppConnections endpoint', () => {
|
||||
it.each([
|
||||
DefaultProjectRole.ADMIN,
|
||||
DefaultProjectRole.EDITOR,
|
||||
DefaultProjectRole.VIEWER,
|
||||
])('Succeeds if user role is %s', async (testRole) => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
|
||||
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: testRole }) as ProjectRole
|
||||
|
||||
const mockProjectMember = createMockProjectMember({
|
||||
userId: mockUser.id,
|
||||
platformId: mockPlatform.id,
|
||||
projectId: mockProject.id,
|
||||
projectRoleId: projectRole.id,
|
||||
})
|
||||
await databaseConnection().getRepository('project_member').save([mockProjectMember])
|
||||
|
||||
const mockToken = await generateMockToken({
|
||||
id: mockUser.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/app-connections',
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
query: {
|
||||
projectId: mockProject.id,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
import { FastifyInstance } from 'fastify'
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { initializeDatabase } from '../../../../src/app/database'
|
||||
import { databaseConnection } from '../../../../src/app/database/database-connection'
|
||||
import { setupServer } from '../../../../src/app/server'
|
||||
|
||||
let app: FastifyInstance | null = null
|
||||
|
||||
beforeAll(async () => {
|
||||
await initializeDatabase({ runMigrations: false })
|
||||
app = await setupServer()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await databaseConnection().destroy()
|
||||
await app?.close()
|
||||
})
|
||||
|
||||
describe('AppSumo API', () => {
|
||||
describe('Action endpoint', () => {
|
||||
it('Activates new accounts', async () => {
|
||||
// arrange
|
||||
const mockEmail = 'mock-email'
|
||||
|
||||
const requestBody = {
|
||||
action: 'activate',
|
||||
plan_id: 'plan_id',
|
||||
uuid: 'uuid',
|
||||
activation_email: mockEmail,
|
||||
}
|
||||
|
||||
const appSumoToken = 'app-sumo-token'
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/appsumo/action',
|
||||
headers: {
|
||||
authorization: `Bearer ${appSumoToken}`,
|
||||
},
|
||||
body: requestBody,
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
expect(response?.statusCode).toBe(StatusCodes.CREATED)
|
||||
expect(responseBody?.message).toBe('success')
|
||||
expect(responseBody?.redirect_url).toBe(
|
||||
`https://cloud.activepieces.com/sign-up?email=${mockEmail}`,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,127 @@
|
||||
import { PlatformRole, PrincipalType } from '@activepieces/shared'
|
||||
import { FastifyInstance } from 'fastify'
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { initializeDatabase } from '../../../../src/app/database'
|
||||
import { databaseConnection } from '../../../../src/app/database/database-connection'
|
||||
import { setupServer } from '../../../../src/app/server'
|
||||
import { generateMockToken } from '../../../helpers/auth'
|
||||
import {
|
||||
createAuditEvent,
|
||||
mockAndSaveBasicSetup,
|
||||
mockBasicUser,
|
||||
} from '../../../helpers/mocks'
|
||||
|
||||
let app: FastifyInstance | null = null
|
||||
|
||||
beforeAll(async () => {
|
||||
await initializeDatabase({ runMigrations: false })
|
||||
app = await setupServer()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await databaseConnection().destroy()
|
||||
await app?.close()
|
||||
})
|
||||
|
||||
describe('Audit Event API', () => {
|
||||
describe('List Audit event API', () => {
|
||||
it('should list audit events', async () => {
|
||||
// arrange
|
||||
const { mockOwner: mockUserOne, mockPlatform: mockPlatformOne } = await mockAndSaveBasicSetup({
|
||||
plan: {
|
||||
auditLogEnabled: true,
|
||||
},
|
||||
})
|
||||
const { mockOwner: mockUserTwo, mockPlatform: mockPlatformTwo, mockProject: mockProjectOne } = await mockAndSaveBasicSetup({
|
||||
|
||||
})
|
||||
|
||||
|
||||
const testToken1 = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockUserOne.id,
|
||||
projectId: mockProjectOne.id,
|
||||
platform: { id: mockPlatformOne.id },
|
||||
})
|
||||
|
||||
const mockAuditEvents1 = [
|
||||
createAuditEvent({
|
||||
platformId: mockPlatformOne.id,
|
||||
userId: mockUserOne.id,
|
||||
}),
|
||||
createAuditEvent({
|
||||
platformId: mockPlatformOne.id,
|
||||
userId: mockUserOne.id,
|
||||
}),
|
||||
]
|
||||
await databaseConnection()
|
||||
.getRepository('audit_event')
|
||||
.save(mockAuditEvents1)
|
||||
|
||||
const mockAuditEvents2 = [
|
||||
createAuditEvent({
|
||||
platformId: mockPlatformTwo.id,
|
||||
userId: mockUserTwo.id,
|
||||
}),
|
||||
createAuditEvent({
|
||||
platformId: mockPlatformTwo.id,
|
||||
userId: mockUserTwo.id,
|
||||
}),
|
||||
]
|
||||
await databaseConnection()
|
||||
.getRepository('audit_event')
|
||||
.save(mockAuditEvents2)
|
||||
|
||||
// act
|
||||
const response1 = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/audit-events',
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken1}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response1?.statusCode).toBe(StatusCodes.OK)
|
||||
const responseBody1 = response1?.json()
|
||||
|
||||
expect(responseBody1.data).toHaveLength(mockAuditEvents1.length)
|
||||
expect(responseBody1?.data).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: mockAuditEvents1[0].id }),
|
||||
expect.objectContaining({ id: mockAuditEvents1[1].id }),
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
it('should return forbidden if the user is not the owner', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockUser.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/audit-events',
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,765 @@
|
||||
import {
|
||||
CustomDomain,
|
||||
OtpType,
|
||||
} from '@activepieces/ee-shared'
|
||||
import {
|
||||
DefaultProjectRole,
|
||||
InvitationStatus,
|
||||
InvitationType,
|
||||
Platform,
|
||||
PlatformPlan,
|
||||
PlatformRole,
|
||||
Project,
|
||||
ProjectRole,
|
||||
ProjectType,
|
||||
User,
|
||||
UserStatus,
|
||||
} from '@activepieces/shared'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import dayjs from 'dayjs'
|
||||
import { FastifyBaseLogger, FastifyInstance } from 'fastify'
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { initializeDatabase } from '../../../../src/app/database'
|
||||
import { databaseConnection } from '../../../../src/app/database/database-connection'
|
||||
import * as emailServiceFile from '../../../../src/app/ee/helper/email/email-service'
|
||||
import { setupServer } from '../../../../src/app/server'
|
||||
import { decodeToken } from '../../../helpers/auth'
|
||||
import {
|
||||
CLOUD_PLATFORM_ID,
|
||||
createMockCustomDomain,
|
||||
createMockPlatform,
|
||||
createMockPlatformPlan,
|
||||
createMockProject,
|
||||
createMockUserInvitation,
|
||||
mockAndSaveBasicSetup,
|
||||
mockBasicUser,
|
||||
} from '../../../helpers/mocks'
|
||||
import {
|
||||
createMockSignInRequest,
|
||||
createMockSignUpRequest,
|
||||
} from '../../../helpers/mocks/authn'
|
||||
|
||||
let app: FastifyInstance | null = null
|
||||
let sendOtpSpy: jest.Mock
|
||||
|
||||
beforeAll(async () => {
|
||||
await initializeDatabase({ runMigrations: false })
|
||||
app = await setupServer()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
sendOtpSpy = jest.fn()
|
||||
jest.spyOn(emailServiceFile, 'emailService').mockImplementation((_log: FastifyBaseLogger) => ({
|
||||
sendOtp: sendOtpSpy,
|
||||
sendInvitation: jest.fn(),
|
||||
sendIssueCreatedNotification: jest.fn(),
|
||||
sendQuotaAlert: jest.fn(),
|
||||
sendTrialReminder: jest.fn(),
|
||||
sendReminderJobHandler: jest.fn(),
|
||||
sendExceedFailureThresholdAlert: jest.fn(),
|
||||
}))
|
||||
|
||||
await databaseConnection().getRepository('flag').createQueryBuilder().delete().execute()
|
||||
await databaseConnection().getRepository('project').createQueryBuilder().delete().execute()
|
||||
await databaseConnection().getRepository('platform').createQueryBuilder().delete().execute()
|
||||
await databaseConnection().getRepository('user').createQueryBuilder().delete().execute()
|
||||
await databaseConnection().getRepository('user_identity').createQueryBuilder().delete().execute()
|
||||
await databaseConnection().getRepository('custom_domain').createQueryBuilder().delete().execute()
|
||||
await databaseConnection().getRepository('user_invitation').createQueryBuilder().delete().execute()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await databaseConnection().destroy()
|
||||
await app?.close()
|
||||
})
|
||||
|
||||
describe('Authentication API', () => {
|
||||
describe('Sign up Endpoint', () => {
|
||||
it('Add new user if the domain is allowed', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockUser, mockCustomDomain } =
|
||||
await createMockPlatformAndDomain({
|
||||
platform: {
|
||||
id: CLOUD_PLATFORM_ID,
|
||||
emailAuthEnabled: true,
|
||||
},
|
||||
plan: {
|
||||
ssoEnabled: false,
|
||||
},
|
||||
})
|
||||
const mockSignUpRequest = createMockSignUpRequest()
|
||||
await databaseConnection()
|
||||
.getRepository('platform')
|
||||
.update(mockPlatform.id, {
|
||||
enforceAllowedAuthDomains: true,
|
||||
allowedAuthDomains: [mockSignUpRequest.email.split('@')[1]],
|
||||
})
|
||||
|
||||
const mockProject = createMockProject({
|
||||
ownerId: mockUser.id,
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
await databaseConnection().getRepository('project').save(mockProject)
|
||||
|
||||
const mockUserInvitation = createMockUserInvitation({
|
||||
platformId: mockPlatform.id,
|
||||
email: mockSignUpRequest.email,
|
||||
platformRole: PlatformRole.ADMIN,
|
||||
type: InvitationType.PLATFORM,
|
||||
status: InvitationStatus.ACCEPTED,
|
||||
created: dayjs().toISOString(),
|
||||
})
|
||||
|
||||
await databaseConnection()
|
||||
.getRepository('user_invitation')
|
||||
.save(mockUserInvitation)
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/authentication/sign-up',
|
||||
body: mockSignUpRequest,
|
||||
headers: {
|
||||
Host: mockCustomDomain.domain,
|
||||
},
|
||||
})
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
})
|
||||
it('Fails If the domain is not allowed', async () => {
|
||||
// arrange
|
||||
const { mockCustomDomain } =
|
||||
await createMockPlatformAndDomain({
|
||||
platform: {
|
||||
emailAuthEnabled: true,
|
||||
enforceAllowedAuthDomains: true,
|
||||
|
||||
allowedAuthDomains: [],
|
||||
|
||||
},
|
||||
plan: {
|
||||
ssoEnabled: true,
|
||||
},
|
||||
})
|
||||
const mockSignUpRequest = createMockSignUpRequest()
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/authentication/sign-up',
|
||||
body: mockSignUpRequest,
|
||||
headers: {
|
||||
Host: mockCustomDomain.domain,
|
||||
},
|
||||
})
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
const responseBody = response?.json()
|
||||
expect(responseBody?.code).toBe('DOMAIN_NOT_ALLOWED')
|
||||
})
|
||||
|
||||
it('Create new user for the cloud user and then ask to verify email if email is not verified', async () => {
|
||||
await createMockPlatformAndDomain({
|
||||
platform: {
|
||||
id: CLOUD_PLATFORM_ID,
|
||||
emailAuthEnabled: true,
|
||||
},
|
||||
plan: {
|
||||
ssoEnabled: false,
|
||||
},
|
||||
})
|
||||
// arrange
|
||||
const mockSignUpRequest = createMockSignUpRequest()
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/authentication/sign-up',
|
||||
body: mockSignUpRequest,
|
||||
headers: {
|
||||
},
|
||||
})
|
||||
|
||||
const responseBody = response?.json()
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
expect(responseBody).toEqual({
|
||||
code: 'EMAIL_IS_NOT_VERIFIED',
|
||||
params: {
|
||||
email: mockSignUpRequest.email.toLocaleLowerCase().trim(),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('Sends a verification email', async () => {
|
||||
// arrange
|
||||
const mockSignUpRequest = createMockSignUpRequest()
|
||||
await createMockPlatformAndDomain({
|
||||
platform: {
|
||||
id: CLOUD_PLATFORM_ID,
|
||||
emailAuthEnabled: true,
|
||||
enforceAllowedAuthDomains: false,
|
||||
},
|
||||
plan: {
|
||||
ssoEnabled: false,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/authentication/sign-up',
|
||||
body: mockSignUpRequest,
|
||||
headers: {
|
||||
},
|
||||
})
|
||||
const responseBody = response?.json()
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
expect(responseBody).toEqual({
|
||||
code: 'EMAIL_IS_NOT_VERIFIED',
|
||||
params: {
|
||||
email: mockSignUpRequest.email.toLocaleLowerCase().trim(),
|
||||
},
|
||||
})
|
||||
|
||||
expect(sendOtpSpy).toHaveBeenCalledTimes(1)
|
||||
expect(sendOtpSpy).toHaveBeenCalledWith({
|
||||
otp: expect.stringMatching(/^([0-9A-F]|-){36}$/i),
|
||||
platformId: expect.any(String),
|
||||
type: OtpType.EMAIL_VERIFICATION,
|
||||
userIdentity: expect.objectContaining({
|
||||
email: mockSignUpRequest.email.trim().toLocaleLowerCase(),
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
it('auto verify invited users to continue platform sign up', async () => {
|
||||
const {
|
||||
mockUser: mockPlatformOwner,
|
||||
mockPlatform,
|
||||
mockCustomDomain,
|
||||
} = await createMockPlatformAndDomain({
|
||||
platform: {
|
||||
},
|
||||
plan: {
|
||||
ssoEnabled: false,
|
||||
projectRolesEnabled: true,
|
||||
},
|
||||
})
|
||||
|
||||
const mockProject = createMockProject({
|
||||
ownerId: mockPlatformOwner.id,
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
await databaseConnection().getRepository('project').save(mockProject)
|
||||
|
||||
const editorRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.EDITOR }) as ProjectRole
|
||||
|
||||
const mockedUpEmail = faker.internet.email()
|
||||
const mockUserInvitation = createMockUserInvitation({
|
||||
projectId: mockProject.id,
|
||||
platformId: mockPlatform.id,
|
||||
email: mockedUpEmail,
|
||||
projectRole: editorRole,
|
||||
type: InvitationType.PROJECT,
|
||||
status: InvitationStatus.ACCEPTED,
|
||||
created: dayjs().toISOString(),
|
||||
})
|
||||
|
||||
await databaseConnection()
|
||||
.getRepository('user_invitation')
|
||||
.save(mockUserInvitation)
|
||||
|
||||
|
||||
const mockSignUpRequest = createMockSignUpRequest({
|
||||
email: mockedUpEmail,
|
||||
})
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/authentication/sign-up',
|
||||
headers: {
|
||||
Host: mockCustomDomain.domain,
|
||||
},
|
||||
body: mockSignUpRequest,
|
||||
})
|
||||
|
||||
const responseBody = response?.json()
|
||||
|
||||
const projects = await databaseConnection().getRepository('project').find({ where: { ownerId: responseBody?.id } })
|
||||
expect(projects.length).toBe(1)
|
||||
expect(projects[0].type).toBe(ProjectType.PERSONAL)
|
||||
|
||||
const teamProject = await databaseConnection().getRepository('project').findOne({ where: { displayName: mockProject.displayName } })
|
||||
expect(teamProject).toBeDefined()
|
||||
|
||||
const projectMember = await databaseConnection().getRepository('project_member').findOne({ where: { projectId: teamProject?.id, userId: responseBody?.id } })
|
||||
|
||||
expect(projectMember).toBeDefined()
|
||||
expect(projectMember?.userId).toBe(responseBody?.id)
|
||||
expect(projectMember?.projectId).toBe(teamProject?.id)
|
||||
expect(projectMember?.platformId).toBe(mockPlatform.id)
|
||||
expect(projectMember?.projectRoleId).toBe(editorRole.id)
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(responseBody?.platformId).toBeDefined()
|
||||
expect(responseBody?.status).toBe('ACTIVE')
|
||||
expect(responseBody?.verified).toBe(true)
|
||||
})
|
||||
|
||||
it('fails to sign up invited user platform if no project exist', async () => {
|
||||
// arrange
|
||||
const { mockCustomDomain } = await createMockPlatformAndDomain({
|
||||
platform: {
|
||||
emailAuthEnabled: true,
|
||||
enforceAllowedAuthDomains: false,
|
||||
},
|
||||
plan: {
|
||||
ssoEnabled: false,
|
||||
},
|
||||
})
|
||||
const mockedUpEmail = faker.internet.email()
|
||||
const mockSignUpRequest = createMockSignUpRequest({
|
||||
email: mockedUpEmail,
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/authentication/sign-up',
|
||||
headers: {
|
||||
Host: mockCustomDomain.domain,
|
||||
},
|
||||
body: mockSignUpRequest,
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(responseBody?.code).toBe('INVITATION_ONLY_SIGN_UP')
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('Sign in Endpoint', () => {
|
||||
it('Fails If the email auth is not enabled', async () => {
|
||||
// arrange
|
||||
|
||||
const rawPassword = faker.internet.password()
|
||||
|
||||
const { mockPlatform, mockCustomDomain } = await createMockPlatformAndDomain({
|
||||
platform: {
|
||||
emailAuthEnabled: false,
|
||||
},
|
||||
plan: {
|
||||
ssoEnabled: true,
|
||||
},
|
||||
})
|
||||
|
||||
const { mockUserIdentity } = await mockBasicUser({
|
||||
user: {
|
||||
status: UserStatus.ACTIVE,
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.ADMIN,
|
||||
},
|
||||
userIdentity: {
|
||||
email: faker.internet.email(),
|
||||
password: rawPassword,
|
||||
verified: true,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
|
||||
const mockSignInRequest = createMockSignInRequest({
|
||||
email: mockUserIdentity.email,
|
||||
password: rawPassword,
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/authentication/sign-in',
|
||||
headers: {
|
||||
Host: mockCustomDomain.domain,
|
||||
},
|
||||
body: mockSignInRequest,
|
||||
})
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(responseBody?.code).toBe('EMAIL_AUTH_DISABLED')
|
||||
})
|
||||
|
||||
it('Fails If the domain is not allowed', async () => {
|
||||
// arrange
|
||||
const mockPlatformId = faker.string.nanoid()
|
||||
const mockPlatformDomain = faker.internet.domainName()
|
||||
|
||||
await createMockPlatformAndDomain({
|
||||
platform: {
|
||||
id: mockPlatformId,
|
||||
allowedAuthDomains: [mockPlatformDomain],
|
||||
enforceAllowedAuthDomains: true,
|
||||
emailAuthEnabled: true,
|
||||
},
|
||||
plan: {
|
||||
ssoEnabled: true,
|
||||
},
|
||||
domain: {
|
||||
domain: mockPlatformDomain,
|
||||
},
|
||||
})
|
||||
const rawPassword = faker.internet.password()
|
||||
const { mockUserIdentity } = await mockBasicUser({
|
||||
user: {
|
||||
status: UserStatus.ACTIVE,
|
||||
platformId: mockPlatformId,
|
||||
platformRole: PlatformRole.ADMIN,
|
||||
},
|
||||
userIdentity: {
|
||||
email: faker.internet.email(),
|
||||
password: rawPassword,
|
||||
verified: true,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
const mockSignInRequest = createMockSignInRequest({
|
||||
email: mockUserIdentity.email,
|
||||
password: rawPassword,
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/authentication/sign-in',
|
||||
headers: {
|
||||
Host: mockPlatformDomain,
|
||||
},
|
||||
body: mockSignInRequest,
|
||||
})
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(responseBody?.code).toBe('DOMAIN_NOT_ALLOWED')
|
||||
})
|
||||
|
||||
it('Logs in existing users', async () => {
|
||||
// arrange
|
||||
const mockEmail = faker.internet.email()
|
||||
const mockPassword = 'password'
|
||||
const { mockPlatform, mockProject } = await createMockPlatformAndDomain({
|
||||
platform: {
|
||||
emailAuthEnabled: true,
|
||||
enforceAllowedAuthDomains: false,
|
||||
},
|
||||
plan: {
|
||||
embeddingEnabled: false,
|
||||
ssoEnabled: false,
|
||||
},
|
||||
})
|
||||
const { mockUser, mockUserIdentity } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
status: UserStatus.ACTIVE,
|
||||
platformRole: PlatformRole.ADMIN,
|
||||
},
|
||||
userIdentity: {
|
||||
email: mockEmail,
|
||||
password: mockPassword,
|
||||
verified: true,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
|
||||
const mockSignInRequest = createMockSignInRequest({
|
||||
email: mockEmail,
|
||||
password: mockPassword,
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/authentication/sign-in',
|
||||
body: mockSignInRequest,
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(responseBody?.id).toBe(mockUser.id)
|
||||
expect(responseBody?.email.toLocaleLowerCase().trim()).toBe(mockEmail.toLocaleLowerCase().trim())
|
||||
expect(responseBody?.firstName).toBe(mockUserIdentity.firstName)
|
||||
expect(responseBody?.lastName).toBe(mockUserIdentity.lastName)
|
||||
expect(responseBody?.trackEvents).toBe(mockUserIdentity.trackEvents)
|
||||
expect(responseBody?.newsLetter).toBe(mockUserIdentity.newsLetter)
|
||||
expect(responseBody?.password).toBeUndefined()
|
||||
expect(responseBody?.status).toBe(mockUser.status)
|
||||
expect(responseBody?.verified).toBe(mockUserIdentity.verified)
|
||||
expect(responseBody?.platformId).toBe(mockPlatform.id)
|
||||
expect(responseBody?.externalId).toBe(null)
|
||||
expect(responseBody?.projectId).toBe(mockProject.id)
|
||||
expect(responseBody?.token).toBeDefined()
|
||||
})
|
||||
|
||||
|
||||
it('Signs in platform users', async () => {
|
||||
// arrange
|
||||
const mockEmail = faker.internet.email()
|
||||
const mockPassword = 'password'
|
||||
const mockPlatformId = faker.string.nanoid()
|
||||
const mockPlatformDomain = faker.internet.domainName()
|
||||
|
||||
await createMockPlatformAndDomain({
|
||||
platform: {
|
||||
id: mockPlatformId,
|
||||
emailAuthEnabled: true,
|
||||
enforceAllowedAuthDomains: false,
|
||||
},
|
||||
plan: {
|
||||
ssoEnabled: false,
|
||||
},
|
||||
domain: {
|
||||
domain: mockPlatformDomain,
|
||||
platformId: mockPlatformId,
|
||||
},
|
||||
})
|
||||
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
status: UserStatus.ACTIVE,
|
||||
platformId: mockPlatformId,
|
||||
platformRole: PlatformRole.ADMIN,
|
||||
},
|
||||
userIdentity: {
|
||||
email: mockEmail,
|
||||
password: mockPassword,
|
||||
verified: true,
|
||||
},
|
||||
})
|
||||
await databaseConnection().getRepository('user').save(mockUser)
|
||||
|
||||
const mockProject = createMockProject({
|
||||
ownerId: mockUser.id,
|
||||
platformId: mockPlatformId,
|
||||
})
|
||||
await databaseConnection().getRepository('project').save(mockProject)
|
||||
|
||||
const mockSignInRequest = createMockSignInRequest({
|
||||
email: mockEmail,
|
||||
password: mockPassword,
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/authentication/sign-in',
|
||||
headers: {
|
||||
Host: mockPlatformDomain,
|
||||
},
|
||||
body: mockSignInRequest,
|
||||
})
|
||||
|
||||
const responseBody = response?.json()
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(responseBody?.platformId).toBe(mockPlatformId)
|
||||
|
||||
const decodedToken = decodeToken(responseBody?.token)
|
||||
expect(decodedToken?.platform?.id).toBe(mockPlatformId)
|
||||
})
|
||||
|
||||
it('Fails to sign in platform users if no project exists', async () => {
|
||||
// arrange
|
||||
|
||||
|
||||
const { mockPlatform, mockCustomDomain } = await createMockPlatformAndDomain({
|
||||
platform: {
|
||||
emailAuthEnabled: true,
|
||||
enforceAllowedAuthDomains: false,
|
||||
},
|
||||
plan: {
|
||||
ssoEnabled: true,
|
||||
},
|
||||
})
|
||||
const mockPassword = 'password'
|
||||
const mockUserIdentityEmail = faker.internet.email()
|
||||
await mockBasicUser({
|
||||
user: {
|
||||
status: UserStatus.ACTIVE,
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
userIdentity: {
|
||||
email: mockUserIdentityEmail,
|
||||
password: mockPassword,
|
||||
verified: true,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
const mockSignInRequest = createMockSignInRequest({
|
||||
email: mockUserIdentityEmail,
|
||||
password: mockPassword,
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/authentication/sign-in',
|
||||
headers: {
|
||||
Host: mockCustomDomain.domain,
|
||||
},
|
||||
body: mockSignInRequest,
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
|
||||
expect(responseBody?.code).toBe('INVITATION_ONLY_SIGN_UP')
|
||||
})
|
||||
|
||||
it('Fails if password doesn\'t match', async () => {
|
||||
// arrange
|
||||
const mockEmail = faker.internet.email()
|
||||
const mockPassword = 'password'
|
||||
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
status: UserStatus.ACTIVE,
|
||||
},
|
||||
userIdentity: {
|
||||
email: mockEmail,
|
||||
password: mockPassword,
|
||||
verified: true,
|
||||
},
|
||||
})
|
||||
|
||||
const mockPlatform = createMockPlatform({
|
||||
id: CLOUD_PLATFORM_ID,
|
||||
ownerId: mockUser.id,
|
||||
})
|
||||
await databaseConnection().getRepository('platform').save(mockPlatform)
|
||||
|
||||
const mockProject = createMockProject({
|
||||
ownerId: mockUser.id,
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
await databaseConnection().getRepository('project').save(mockProject)
|
||||
|
||||
const mockSignInRequest = createMockSignInRequest({
|
||||
email: mockEmail,
|
||||
password: 'wrong password',
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/authentication/sign-in',
|
||||
body: mockSignInRequest,
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.UNAUTHORIZED)
|
||||
const responseBody = response?.json()
|
||||
expect(responseBody?.code).toBe('INVALID_CREDENTIALS')
|
||||
})
|
||||
|
||||
it('Fails if user status is INACTIVE', async () => {
|
||||
// arrange
|
||||
const mockEmail = faker.internet.email()
|
||||
const mockPassword = 'password'
|
||||
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
status: UserStatus.INACTIVE,
|
||||
platformRole: PlatformRole.ADMIN,
|
||||
},
|
||||
userIdentity: {
|
||||
email: mockEmail,
|
||||
password: mockPassword,
|
||||
verified: true,
|
||||
},
|
||||
})
|
||||
|
||||
const mockPlatform = createMockPlatform({
|
||||
ownerId: mockUser.id,
|
||||
emailAuthEnabled: true,
|
||||
enforceAllowedAuthDomains: false,
|
||||
})
|
||||
await databaseConnection().getRepository('platform').save(mockPlatform)
|
||||
|
||||
const mockPlatformPlan = createMockPlatformPlan({
|
||||
platformId: mockPlatform.id,
|
||||
ssoEnabled: false,
|
||||
})
|
||||
await databaseConnection().getRepository('platform_plan').save(mockPlatformPlan)
|
||||
|
||||
await databaseConnection().getRepository('user').update(mockUser.id, {
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
|
||||
const mockProject = createMockProject({
|
||||
ownerId: mockUser.id,
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
await databaseConnection().getRepository('project').save(mockProject)
|
||||
|
||||
const mockSignInRequest = createMockSignInRequest({
|
||||
email: mockEmail,
|
||||
password: mockPassword,
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/authentication/sign-in',
|
||||
body: mockSignInRequest,
|
||||
})
|
||||
|
||||
const responseBody = response?.json()
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.UNAUTHORIZED)
|
||||
|
||||
expect(responseBody?.code).toBe('AUTHENTICATION')
|
||||
expect(responseBody?.params.message).toBe('No platform found for identity')
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
async function createMockPlatformAndDomain({ platform, domain, plan }: { platform: Partial<Platform>, domain?: Partial<CustomDomain>, plan?: Partial<PlatformPlan> }): Promise<{
|
||||
mockUser: User
|
||||
mockPlatform: Platform
|
||||
mockCustomDomain: CustomDomain
|
||||
mockProject: Project
|
||||
}> {
|
||||
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup({
|
||||
platform,
|
||||
})
|
||||
const mockCustomDomain = createMockCustomDomain({
|
||||
platformId: mockPlatform.id,
|
||||
...domain,
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('custom_domain')
|
||||
.save(mockCustomDomain)
|
||||
const mockPlatformPlan = createMockPlatformPlan({
|
||||
platformId: mockPlatform.id,
|
||||
...plan,
|
||||
})
|
||||
await databaseConnection().getRepository('platform_plan').upsert(mockPlatformPlan, ['platformId'])
|
||||
return { mockUser: mockOwner, mockPlatform, mockCustomDomain, mockProject }
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
import { OtpState, OtpType } from '@activepieces/ee-shared'
|
||||
import { UserStatus } from '@activepieces/shared'
|
||||
import dayjs from 'dayjs'
|
||||
import { FastifyInstance } from 'fastify'
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { initializeDatabase } from '../../../../src/app/database'
|
||||
import { databaseConnection } from '../../../../src/app/database/database-connection'
|
||||
import { setupServer } from '../../../../src/app/server'
|
||||
import { createMockOtp, mockBasicUser } from '../../../helpers/mocks'
|
||||
|
||||
let app: FastifyInstance | null = null
|
||||
|
||||
beforeAll(async () => {
|
||||
await initializeDatabase({ runMigrations: false })
|
||||
app = await setupServer()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await databaseConnection().destroy()
|
||||
await app?.close()
|
||||
})
|
||||
|
||||
describe('Enterprise Local Authn API', () => {
|
||||
describe('Verify Email Endpoint', () => {
|
||||
it('Verifies user', async () => {
|
||||
const { mockUserIdentity } = await mockBasicUser({
|
||||
user: {
|
||||
status: UserStatus.ACTIVE,
|
||||
},
|
||||
userIdentity: {
|
||||
verified: false,
|
||||
},
|
||||
})
|
||||
const mockOtp = createMockOtp({
|
||||
identityId: mockUserIdentity.id,
|
||||
type: OtpType.EMAIL_VERIFICATION,
|
||||
state: OtpState.PENDING,
|
||||
})
|
||||
await databaseConnection().getRepository('otp').save(mockOtp)
|
||||
|
||||
const mockVerifyEmailRequest = {
|
||||
identityId: mockUserIdentity.id,
|
||||
otp: mockOtp.value,
|
||||
}
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/authn/local/verify-email',
|
||||
body: mockVerifyEmailRequest,
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(response?.body).toBe('')
|
||||
|
||||
const userIdentity = await databaseConnection()
|
||||
.getRepository('user_identity')
|
||||
.findOneBy({ id: mockUserIdentity.id })
|
||||
expect(userIdentity?.verified).toBe(true)
|
||||
const otp = await databaseConnection()
|
||||
.getRepository('otp')
|
||||
.findOneBy({ id: mockOtp.id })
|
||||
expect(otp?.state).toBe(OtpState.CONFIRMED)
|
||||
})
|
||||
|
||||
it('Fails if OTP is wrong', async () => {
|
||||
const { mockUserIdentity } = await mockBasicUser({
|
||||
user: {
|
||||
status: UserStatus.ACTIVE,
|
||||
},
|
||||
userIdentity: {
|
||||
verified: false,
|
||||
},
|
||||
})
|
||||
const correctOtp = '123456'
|
||||
const mockOtp = createMockOtp({
|
||||
identityId: mockUserIdentity.id,
|
||||
type: OtpType.EMAIL_VERIFICATION,
|
||||
value: correctOtp,
|
||||
state: OtpState.PENDING,
|
||||
})
|
||||
await databaseConnection().getRepository('otp').save(mockOtp)
|
||||
|
||||
const incorrectOtp = '654321'
|
||||
const mockVerifyEmailRequest = {
|
||||
identityId: mockUserIdentity.id,
|
||||
otp: incorrectOtp,
|
||||
}
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/authn/local/verify-email',
|
||||
body: mockVerifyEmailRequest,
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.GONE)
|
||||
const responseBody = response?.json()
|
||||
expect(responseBody?.code).toBe('INVALID_OTP')
|
||||
|
||||
const userIdentity = await databaseConnection()
|
||||
.getRepository('user_identity')
|
||||
.findOneBy({ id: mockUserIdentity.id })
|
||||
expect(userIdentity?.verified).toBe(false)
|
||||
})
|
||||
|
||||
it('Fails if OTP has expired', async () => {
|
||||
const { mockUserIdentity } = await mockBasicUser({
|
||||
user: {
|
||||
status: UserStatus.ACTIVE,
|
||||
},
|
||||
userIdentity: {
|
||||
verified: false,
|
||||
},
|
||||
})
|
||||
|
||||
const mockOtp = createMockOtp({
|
||||
identityId: mockUserIdentity.id,
|
||||
type: OtpType.EMAIL_VERIFICATION,
|
||||
updated: dayjs().subtract(31, 'minutes').toISOString(),
|
||||
state: OtpState.PENDING,
|
||||
})
|
||||
await databaseConnection().getRepository('otp').save(mockOtp)
|
||||
|
||||
const mockVerifyEmailRequest = {
|
||||
identityId: mockUserIdentity.id,
|
||||
otp: mockOtp.value,
|
||||
}
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/authn/local/verify-email',
|
||||
body: mockVerifyEmailRequest,
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.GONE)
|
||||
const responseBody = response?.json()
|
||||
expect(responseBody?.code).toBe('INVALID_OTP')
|
||||
|
||||
const userIdentity = await databaseConnection()
|
||||
.getRepository('user_identity')
|
||||
.findOneBy({ id: mockUserIdentity.id })
|
||||
expect(userIdentity?.verified).toBe(false)
|
||||
})
|
||||
|
||||
it('Fails if OTP was confirmed before', async () => {
|
||||
const { mockUserIdentity } = await mockBasicUser({
|
||||
user: {
|
||||
status: UserStatus.ACTIVE,
|
||||
},
|
||||
userIdentity: {
|
||||
verified: false,
|
||||
},
|
||||
})
|
||||
|
||||
const mockOtp = createMockOtp({
|
||||
identityId: mockUserIdentity.id,
|
||||
type: OtpType.EMAIL_VERIFICATION,
|
||||
state: OtpState.CONFIRMED,
|
||||
})
|
||||
await databaseConnection().getRepository('otp').save(mockOtp)
|
||||
|
||||
const mockVerifyEmailRequest = {
|
||||
identityId: mockUserIdentity.id,
|
||||
otp: mockOtp.value,
|
||||
}
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/authn/local/verify-email',
|
||||
body: mockVerifyEmailRequest,
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.GONE)
|
||||
const responseBody = response?.json()
|
||||
expect(responseBody?.code).toBe('INVALID_OTP')
|
||||
|
||||
const userIdentity = await databaseConnection()
|
||||
.getRepository('user_identity')
|
||||
.findOneBy({ id: mockUserIdentity.id })
|
||||
expect(userIdentity?.verified).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Reset Password Endpoint', () => {
|
||||
it('Updates user password', async () => {
|
||||
const { mockUserIdentity } = await mockBasicUser({
|
||||
userIdentity: { },
|
||||
})
|
||||
|
||||
const mockOtp = createMockOtp({
|
||||
identityId: mockUserIdentity.id,
|
||||
type: OtpType.PASSWORD_RESET,
|
||||
state: OtpState.PENDING,
|
||||
})
|
||||
await databaseConnection().getRepository('otp').save(mockOtp)
|
||||
|
||||
const mockResetPasswordRequest = {
|
||||
identityId: mockUserIdentity.id,
|
||||
otp: mockOtp.value,
|
||||
newPassword: 'newPassword',
|
||||
}
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/authn/local/reset-password',
|
||||
body: mockResetPasswordRequest,
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(response?.body).toBe('')
|
||||
|
||||
const userIdentity = await databaseConnection()
|
||||
.getRepository('user_identity')
|
||||
.findOneBy({ id: mockUserIdentity.id })
|
||||
expect(userIdentity?.password).not.toBe(mockUserIdentity.password)
|
||||
})
|
||||
|
||||
it('Fails if OTP is wrong', async () => {
|
||||
const { mockUserIdentity } = await mockBasicUser({
|
||||
|
||||
})
|
||||
|
||||
const correctOtp = '123456'
|
||||
const mockOtp = createMockOtp({
|
||||
identityId: mockUserIdentity.id,
|
||||
type: OtpType.PASSWORD_RESET,
|
||||
value: correctOtp,
|
||||
})
|
||||
await databaseConnection().getRepository('otp').save(mockOtp)
|
||||
|
||||
const incorrectOtp = '654321'
|
||||
const mockResetPasswordRequest = {
|
||||
identityId: mockUserIdentity.id,
|
||||
otp: incorrectOtp,
|
||||
newPassword: 'newPassword',
|
||||
}
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/authn/local/reset-password',
|
||||
body: mockResetPasswordRequest,
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.GONE)
|
||||
const responseBody = response?.json()
|
||||
expect(responseBody?.code).toBe('INVALID_OTP')
|
||||
|
||||
const userIdentity = await databaseConnection()
|
||||
.getRepository('user_identity')
|
||||
.findOneBy({ id: mockUserIdentity.id })
|
||||
expect(userIdentity?.password).toBe(mockUserIdentity.password)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,685 @@
|
||||
import {
|
||||
ActivepiecesError,
|
||||
ALL_PRINCIPAL_TYPES,
|
||||
apId,
|
||||
EndpointScope,
|
||||
ErrorCode,
|
||||
Principal,
|
||||
PrincipalType,
|
||||
} from '@activepieces/shared'
|
||||
import { FastifyInstance, FastifyRequest } from 'fastify'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { securityHandlerChain } from '../../../../src/app/core/security/security-handler-chain'
|
||||
import { initializeDatabase } from '../../../../src/app/database'
|
||||
import { databaseConnection } from '../../../../src/app/database/database-connection'
|
||||
import { setupServer } from '../../../../src/app/server'
|
||||
import { generateMockToken } from '../../../helpers/auth'
|
||||
import {
|
||||
createMockFlow,
|
||||
mockAndSaveBasicSetup,
|
||||
mockAndSaveBasicSetupWithApiKey,
|
||||
} from '../../../helpers/mocks'
|
||||
|
||||
let app: FastifyInstance | null = null
|
||||
|
||||
beforeAll(async () => {
|
||||
await initializeDatabase({ runMigrations: false })
|
||||
app = await setupServer()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await databaseConnection().destroy()
|
||||
await app?.close()
|
||||
})
|
||||
|
||||
describe('API Security', () => {
|
||||
describe('Webhook Authentication', () => {
|
||||
it('Skips principal authentication and authorization for webhook routes', async () => {
|
||||
// arrange
|
||||
const routes = [
|
||||
'/v1/webhooks',
|
||||
'/v1/webhooks/:flowId',
|
||||
'/v1/webhooks/:flowId/simulate',
|
||||
'/v1/webhooks/:flowId/sync',
|
||||
]
|
||||
for (const route of routes) {
|
||||
const mockRequest = {
|
||||
method: 'POST',
|
||||
routeOptions: {
|
||||
url: route,
|
||||
config: {
|
||||
skipAuth: true,
|
||||
allowedPrincipals: ALL_PRINCIPAL_TYPES,
|
||||
},
|
||||
},
|
||||
headers: {
|
||||
|
||||
},
|
||||
} as unknown as FastifyRequest
|
||||
|
||||
// act
|
||||
const result = securityHandlerChain(mockRequest)
|
||||
|
||||
// assert
|
||||
await expect(result).resolves.toBeUndefined()
|
||||
expect(mockRequest.principal.type).toEqual(PrincipalType.UNKNOWN)
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('Platform API Key Authentication', () => {
|
||||
it('Authenticates service principals', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockApiKey } =
|
||||
await mockAndSaveBasicSetupWithApiKey()
|
||||
|
||||
const mockRequest = {
|
||||
method: 'GET',
|
||||
routeOptions: {
|
||||
url: '/v1/flows',
|
||||
config: {
|
||||
allowedPrincipals: [PrincipalType.SERVICE],
|
||||
scope: EndpointScope.PLATFORM,
|
||||
},
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${mockApiKey.value}`,
|
||||
},
|
||||
} as unknown as FastifyRequest
|
||||
|
||||
// act
|
||||
const result = securityHandlerChain(mockRequest)
|
||||
|
||||
// assert
|
||||
await expect(result).resolves.toBeUndefined()
|
||||
|
||||
expect(mockRequest.principal).toEqual(
|
||||
expect.objectContaining({
|
||||
id: mockApiKey.id,
|
||||
type: PrincipalType.SERVICE,
|
||||
projectId: expect.stringMatching(/ANONYMOUS_.{21}/),
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('Gets projectId from body if endpoint scope is PROJECT', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject, mockApiKey } =
|
||||
await mockAndSaveBasicSetupWithApiKey()
|
||||
|
||||
const mockRequest = {
|
||||
method: 'GET',
|
||||
routeOptions: {
|
||||
url: '/v1/flows',
|
||||
config: {
|
||||
allowedPrincipals: [PrincipalType.SERVICE],
|
||||
scope: EndpointScope.PROJECT,
|
||||
},
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${mockApiKey.value}`,
|
||||
},
|
||||
body: {
|
||||
projectId: mockProject.id,
|
||||
},
|
||||
} as unknown as FastifyRequest
|
||||
|
||||
// act
|
||||
const result = securityHandlerChain(mockRequest)
|
||||
|
||||
// assert
|
||||
await expect(result).resolves.toBeUndefined()
|
||||
|
||||
expect(mockRequest.principal).toEqual(
|
||||
expect.objectContaining({
|
||||
id: mockApiKey.id,
|
||||
type: PrincipalType.SERVICE,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('Gets projectId from query if endpoint scope is PROJECT', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject, mockApiKey } =
|
||||
await mockAndSaveBasicSetupWithApiKey()
|
||||
|
||||
const mockRequest = {
|
||||
method: 'GET',
|
||||
routeOptions: {
|
||||
url: '/v1/flows',
|
||||
config: {
|
||||
allowedPrincipals: [PrincipalType.SERVICE],
|
||||
scope: EndpointScope.PROJECT,
|
||||
},
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${mockApiKey.value}`,
|
||||
},
|
||||
query: {
|
||||
projectId: mockProject.id,
|
||||
},
|
||||
} as unknown as FastifyRequest
|
||||
|
||||
// act
|
||||
const result = securityHandlerChain(mockRequest)
|
||||
|
||||
// assert
|
||||
await expect(result).resolves.toBeUndefined()
|
||||
|
||||
expect(mockRequest.principal).toEqual(
|
||||
expect.objectContaining({
|
||||
id: mockApiKey.id,
|
||||
type: PrincipalType.SERVICE,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('extracts projectId from resource if endpoint scope is PROJECT', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject, mockApiKey } =
|
||||
await mockAndSaveBasicSetupWithApiKey()
|
||||
const mockFlow = createMockFlow({ projectId: mockProject.id })
|
||||
|
||||
await databaseConnection().getRepository('flow').save([mockFlow])
|
||||
|
||||
const mockRequest = {
|
||||
method: 'GET',
|
||||
routeOptions: {
|
||||
url: '/v1/flows/:id',
|
||||
config: {
|
||||
allowedPrincipals: [PrincipalType.SERVICE],
|
||||
scope: EndpointScope.PROJECT,
|
||||
},
|
||||
},
|
||||
params: {
|
||||
id: mockFlow.id,
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${mockApiKey.value}`,
|
||||
},
|
||||
} as unknown as FastifyRequest
|
||||
|
||||
// act
|
||||
const result = securityHandlerChain(mockRequest)
|
||||
|
||||
// assert
|
||||
await expect(result).resolves.toBeUndefined()
|
||||
|
||||
expect(mockRequest.principal).toEqual(
|
||||
expect.objectContaining({
|
||||
id: mockApiKey.id,
|
||||
type: PrincipalType.SERVICE,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('Fails if API key and project don\'t belong to same platform if endpoint scope is PROJECT', async () => {
|
||||
// arrange
|
||||
const { mockApiKey } = await mockAndSaveBasicSetupWithApiKey()
|
||||
const { mockProject: mockOtherProject } = await mockAndSaveBasicSetup()
|
||||
|
||||
const mockRequest = {
|
||||
method: 'GET',
|
||||
routeOptions: {
|
||||
url: '/v1/flows',
|
||||
config: {
|
||||
allowedPrincipals: [PrincipalType.SERVICE],
|
||||
scope: EndpointScope.PROJECT,
|
||||
},
|
||||
},
|
||||
query: {
|
||||
projectId: mockOtherProject.id,
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${mockApiKey.value}`,
|
||||
},
|
||||
} as unknown as FastifyRequest
|
||||
|
||||
// act
|
||||
const result = securityHandlerChain(mockRequest)
|
||||
|
||||
// assert
|
||||
await expect(result).rejects.toEqual(
|
||||
new ActivepiecesError({
|
||||
code: ErrorCode.AUTHORIZATION,
|
||||
params: {
|
||||
message: 'invalid project id',
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('Fails if no projectId is extracted from request or resource and endpoint scope is PROJECT', async () => {
|
||||
// arrange
|
||||
const { mockApiKey } = await mockAndSaveBasicSetupWithApiKey()
|
||||
|
||||
const mockRequest = {
|
||||
method: 'GET',
|
||||
routeOptions: {
|
||||
url: '/v1/flows',
|
||||
config: {
|
||||
allowedPrincipals: [PrincipalType.SERVICE],
|
||||
scope: EndpointScope.PROJECT,
|
||||
},
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${mockApiKey.value}`,
|
||||
},
|
||||
} as unknown as FastifyRequest
|
||||
|
||||
// act
|
||||
const result = securityHandlerChain(mockRequest)
|
||||
|
||||
// assert
|
||||
await expect(result).rejects.toEqual(
|
||||
new ActivepiecesError({
|
||||
code: ErrorCode.VALIDATION,
|
||||
params: {
|
||||
message: 'invalid project id',
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('Fails if project with extracted id doesn\'t exist and endpoint scope is PROJECT', async () => {
|
||||
// arrange
|
||||
const mockNonExistentProjectId = apId()
|
||||
const { mockApiKey } = await mockAndSaveBasicSetupWithApiKey()
|
||||
|
||||
const mockRequest = {
|
||||
method: 'GET',
|
||||
routeOptions: {
|
||||
url: '/v1/flows',
|
||||
config: {
|
||||
allowedPrincipals: [PrincipalType.SERVICE],
|
||||
scope: EndpointScope.PROJECT,
|
||||
},
|
||||
},
|
||||
query: {
|
||||
projectId: mockNonExistentProjectId,
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${mockApiKey.value}`,
|
||||
},
|
||||
} as unknown as FastifyRequest
|
||||
|
||||
// act
|
||||
const result = securityHandlerChain(mockRequest)
|
||||
|
||||
// assert
|
||||
await expect(result).rejects.toEqual(
|
||||
new ActivepiecesError({
|
||||
code: ErrorCode.AUTHORIZATION,
|
||||
params: {
|
||||
message: 'invalid project id',
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('Fails if API key doesn\'t exist', async () => {
|
||||
// arrange
|
||||
const mockNonExistentApiKey = '123'
|
||||
|
||||
const mockRequest = {
|
||||
method: 'POST',
|
||||
routeOptions: {
|
||||
url: '/v1/flows',
|
||||
config: {
|
||||
allowedPrincipals: [PrincipalType.SERVICE],
|
||||
scope: EndpointScope.PLATFORM,
|
||||
},
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${mockNonExistentApiKey}`,
|
||||
},
|
||||
} as unknown as FastifyRequest
|
||||
|
||||
// act
|
||||
const result = securityHandlerChain(mockRequest)
|
||||
|
||||
// assert
|
||||
await expect(result).rejects.toEqual(
|
||||
new ActivepiecesError({
|
||||
code: ErrorCode.INVALID_BEARER_TOKEN,
|
||||
params: {
|
||||
message: 'invalid access token',
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('Fails if route doesn\'t allow SERVICE principals', async () => {
|
||||
// arrange
|
||||
const { mockApiKey } = await mockAndSaveBasicSetupWithApiKey()
|
||||
|
||||
const mockRequest = {
|
||||
method: 'POST',
|
||||
routeOptions: {
|
||||
url: '/v1/flows',
|
||||
config: {
|
||||
allowedPrincipals: [PrincipalType.USER],
|
||||
scope: EndpointScope.PLATFORM,
|
||||
},
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${mockApiKey.value}`,
|
||||
},
|
||||
} as unknown as FastifyRequest
|
||||
|
||||
// act
|
||||
const result = securityHandlerChain(mockRequest)
|
||||
|
||||
// assert
|
||||
await expect(result).rejects.toEqual(
|
||||
new ActivepiecesError({
|
||||
code: ErrorCode.AUTHORIZATION,
|
||||
params: {
|
||||
message: 'invalid route for principal type',
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Access Token Authentication', () => {
|
||||
|
||||
it('Session expirey for Users', async () => {
|
||||
// arrange
|
||||
const { mockOwner, mockPlatform, mockProject, mockUserIdentity } = await mockAndSaveBasicSetupWithApiKey()
|
||||
|
||||
await databaseConnection().getRepository('user_identity').update(mockUserIdentity.id, {
|
||||
tokenVersion: nanoid(),
|
||||
})
|
||||
const mockPrincipal: Principal = {
|
||||
id: mockOwner.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
}
|
||||
|
||||
const mockAccessToken = await generateMockToken(mockPrincipal)
|
||||
|
||||
const mockRequest = {
|
||||
method: 'GET',
|
||||
routeOptions: {
|
||||
url: '/v1/flows',
|
||||
config: {},
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${mockAccessToken}`,
|
||||
},
|
||||
} as unknown as FastifyRequest
|
||||
|
||||
// act
|
||||
const result = securityHandlerChain(mockRequest)
|
||||
|
||||
// assert
|
||||
await expect(result).rejects.toEqual(
|
||||
new ActivepiecesError({
|
||||
code: ErrorCode.SESSION_EXPIRED,
|
||||
params: {
|
||||
message: 'The session has expired.',
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('Authenticates users', async () => {
|
||||
// arrange
|
||||
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetupWithApiKey()
|
||||
|
||||
const mockPrincipal: Principal = {
|
||||
id: mockOwner.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
}
|
||||
|
||||
const mockAccessToken = await generateMockToken(mockPrincipal)
|
||||
|
||||
const mockRequest = {
|
||||
method: 'GET',
|
||||
routeOptions: {
|
||||
url: '/v1/flows',
|
||||
config: {
|
||||
allowedPrincipals: [PrincipalType.USER],
|
||||
},
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${mockAccessToken}`,
|
||||
},
|
||||
} as unknown as FastifyRequest
|
||||
|
||||
// act
|
||||
const result = securityHandlerChain(mockRequest)
|
||||
|
||||
// assert
|
||||
await expect(result).resolves.toBeUndefined()
|
||||
|
||||
expect(mockRequest.principal).toEqual(
|
||||
expect.objectContaining({
|
||||
id: mockPrincipal.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockPrincipal.projectId,
|
||||
platform: {
|
||||
id: mockPrincipal.platform.id,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('Fails if route disallows USER principal type', async () => {
|
||||
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetupWithApiKey()
|
||||
|
||||
// arrange
|
||||
const mockPrincipal: Principal = {
|
||||
id: mockOwner.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
}
|
||||
const mockAccessToken = await generateMockToken(mockPrincipal)
|
||||
|
||||
const mockRequest = {
|
||||
method: 'GET',
|
||||
routeOptions: {
|
||||
url: '/v1/flows',
|
||||
config: {
|
||||
allowedPrincipals: [PrincipalType.SERVICE],
|
||||
},
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${mockAccessToken}`,
|
||||
},
|
||||
} as unknown as FastifyRequest
|
||||
|
||||
// act
|
||||
const result = securityHandlerChain(mockRequest)
|
||||
|
||||
// assert
|
||||
await expect(result).rejects.toEqual(
|
||||
new ActivepiecesError({
|
||||
code: ErrorCode.AUTHORIZATION,
|
||||
params: {
|
||||
message: 'invalid route for principal type',
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('Fails if projectId in query doesn\'t match principal projectId', async () => {
|
||||
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetupWithApiKey()
|
||||
|
||||
const mockPrincipal: Principal = {
|
||||
id: mockOwner.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
}
|
||||
|
||||
// arrange
|
||||
const mockOtherProjectId = apId()
|
||||
const mockAccessToken = await generateMockToken(mockPrincipal)
|
||||
|
||||
const mockRequest = {
|
||||
method: 'GET',
|
||||
routeOptions: {
|
||||
url: '/v1/flows',
|
||||
config: {
|
||||
allowedPrincipals: [PrincipalType.USER],
|
||||
},
|
||||
},
|
||||
query: {
|
||||
projectId: mockOtherProjectId,
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${mockAccessToken}`,
|
||||
},
|
||||
} as unknown as FastifyRequest
|
||||
|
||||
// act
|
||||
const result = securityHandlerChain(mockRequest)
|
||||
|
||||
// assert
|
||||
await expect(result).rejects.toEqual(
|
||||
new ActivepiecesError({
|
||||
code: ErrorCode.AUTHORIZATION,
|
||||
params: {
|
||||
message: 'invalid project id',
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('Fails if projectId in body doesn\'t match principal projectId', async () => {
|
||||
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetupWithApiKey()
|
||||
|
||||
const mockPrincipal: Principal = {
|
||||
id: mockOwner.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
}
|
||||
|
||||
// arrange
|
||||
const mockOtherProjectId = apId()
|
||||
const mockAccessToken = await generateMockToken(mockPrincipal)
|
||||
|
||||
const mockRequest = {
|
||||
method: 'GET',
|
||||
routeOptions: {
|
||||
url: '/v1/flows',
|
||||
config: {
|
||||
allowedPrincipals: [PrincipalType.USER],
|
||||
},
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${mockAccessToken}`,
|
||||
},
|
||||
body: {
|
||||
projectId: mockOtherProjectId,
|
||||
},
|
||||
} as unknown as FastifyRequest
|
||||
|
||||
// act
|
||||
const result = securityHandlerChain(mockRequest)
|
||||
|
||||
// assert
|
||||
await expect(result).rejects.toEqual(
|
||||
new ActivepiecesError({
|
||||
code: ErrorCode.AUTHORIZATION,
|
||||
params: {
|
||||
message: 'invalid project id',
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Anonymous authentication', () => {
|
||||
it('Enables access to non authenticated routes', async () => {
|
||||
// arrange
|
||||
const nonAuthenticatedRoute = '/v1/docs'
|
||||
|
||||
const mockRequest = {
|
||||
method: 'GET',
|
||||
routeOptions: {
|
||||
url: nonAuthenticatedRoute,
|
||||
config: {
|
||||
allowedPrincipals: ALL_PRINCIPAL_TYPES,
|
||||
},
|
||||
},
|
||||
headers: {},
|
||||
} as unknown as FastifyRequest
|
||||
|
||||
// act
|
||||
const result = securityHandlerChain(mockRequest)
|
||||
|
||||
// assert
|
||||
await expect(result).resolves.toBeUndefined()
|
||||
|
||||
expect(mockRequest.principal).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.stringMatching(/ANONYMOUS_.{21}/),
|
||||
type: PrincipalType.UNKNOWN,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('Fails if route is authenticated', async () => {
|
||||
// arrange
|
||||
const authenticatedRoute = '/v1/flows'
|
||||
|
||||
const mockRequest = {
|
||||
method: 'GET',
|
||||
routeOptions: {
|
||||
url: authenticatedRoute,
|
||||
config: {
|
||||
allowedPrincipals: [PrincipalType.USER],
|
||||
},
|
||||
},
|
||||
headers: {},
|
||||
} as unknown as FastifyRequest
|
||||
|
||||
// act
|
||||
const result = securityHandlerChain(mockRequest)
|
||||
|
||||
// assert
|
||||
await expect(result).rejects.toEqual(
|
||||
new ActivepiecesError({
|
||||
code: ErrorCode.AUTHORIZATION,
|
||||
params: {
|
||||
message: 'invalid route for principal type',
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,224 @@
|
||||
import { AddDomainRequest, CustomDomainStatus } from '@activepieces/ee-shared'
|
||||
import { PlatformRole, PrincipalType } from '@activepieces/shared'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { FastifyInstance } from 'fastify'
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { initializeDatabase } from '../../../../src/app/database'
|
||||
import { databaseConnection } from '../../../../src/app/database/database-connection'
|
||||
import { setupServer } from '../../../../src/app/server'
|
||||
import { generateMockToken } from '../../../helpers/auth'
|
||||
import {
|
||||
createMockCustomDomain,
|
||||
mockAndSaveBasicSetup,
|
||||
mockBasicUser } from '../../../helpers/mocks'
|
||||
|
||||
let app: FastifyInstance | null = null
|
||||
|
||||
beforeAll(async () => {
|
||||
await initializeDatabase({ runMigrations: false })
|
||||
app = await setupServer()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await databaseConnection().destroy()
|
||||
await app?.close()
|
||||
})
|
||||
|
||||
describe('Custom Domain API', () => {
|
||||
describe('Add Custom Domain API', () => {
|
||||
it('should create a new custom domain', async () => {
|
||||
// arrange
|
||||
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockOwner.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
const request: AddDomainRequest = {
|
||||
domain: faker.internet.domainName(),
|
||||
}
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/custom-domains',
|
||||
body: request,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.CREATED)
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(responseBody.domain).toBe(request.domain)
|
||||
expect(responseBody.status).toBe(CustomDomainStatus.PENDING)
|
||||
})
|
||||
|
||||
it('should fail if user is not platform owner', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
|
||||
const { mockUser: nonOwnerUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: nonOwnerUser.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
const request: AddDomainRequest = {
|
||||
domain: faker.internet.domainName(),
|
||||
}
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/custom-domains',
|
||||
body: request,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
})
|
||||
|
||||
describe('List Custom Domain API', () => {
|
||||
it('should list custom domains', async () => {
|
||||
// arrange
|
||||
const { mockOwner: mockUserOne, mockPlatform: mockPlatformOne, mockProject: mockProjectOne } = await mockAndSaveBasicSetup()
|
||||
const { mockPlatform: mockPlatformTwo } = await mockAndSaveBasicSetup()
|
||||
|
||||
const testToken1 = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockUserOne.id,
|
||||
projectId: mockProjectOne.id,
|
||||
platform: { id: mockPlatformOne.id },
|
||||
})
|
||||
|
||||
const mockCustomDomains1 = [
|
||||
createMockCustomDomain({
|
||||
platformId: mockPlatformOne.id,
|
||||
domain: faker.internet.domainName(),
|
||||
}),
|
||||
createMockCustomDomain({
|
||||
platformId: mockPlatformOne.id,
|
||||
domain: faker.internet.domainName(),
|
||||
}),
|
||||
]
|
||||
await databaseConnection()
|
||||
.getRepository('custom_domain')
|
||||
.save(mockCustomDomains1)
|
||||
|
||||
const mockCustomDomains2 = [
|
||||
createMockCustomDomain({
|
||||
platformId: mockPlatformTwo.id,
|
||||
domain: faker.internet.domainName(),
|
||||
}),
|
||||
]
|
||||
await databaseConnection()
|
||||
.getRepository('custom_domain')
|
||||
.save(mockCustomDomains2)
|
||||
|
||||
// act
|
||||
const response1 = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/custom-domains',
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken1}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response1?.statusCode).toBe(StatusCodes.OK)
|
||||
const responseBody1 = response1?.json()
|
||||
|
||||
expect(responseBody1.data).toHaveLength(mockCustomDomains1.length)
|
||||
expect(responseBody1?.data).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: mockCustomDomains1[0].id }),
|
||||
expect.objectContaining({ id: mockCustomDomains1[1].id }),
|
||||
]),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Delete Custom Domain API', () => {
|
||||
it('should delete a custom domain', async () => {
|
||||
// arrange
|
||||
const { mockOwner: mockUserOne, mockPlatform: mockPlatformOne, mockProject: mockProjectOne } = await mockAndSaveBasicSetup()
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockUserOne.id,
|
||||
projectId: mockProjectOne.id,
|
||||
platform: { id: mockPlatformOne.id },
|
||||
})
|
||||
|
||||
const customDomain = createMockCustomDomain({
|
||||
platformId: mockPlatformOne.id,
|
||||
domain: faker.internet.domainName(),
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('custom_domain')
|
||||
.save(customDomain)
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/custom-domains/${customDomain.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
})
|
||||
|
||||
it('should fail to delete a custom domain if user is not platform owner', async () => {
|
||||
const { mockPlatform: mockPlatformOne, mockProject: mockProjectOne } = await mockAndSaveBasicSetup()
|
||||
|
||||
const { mockUser: nonOwnerUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatformOne.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: nonOwnerUser.id,
|
||||
projectId: mockProjectOne.id,
|
||||
platform: { id: mockPlatformOne.id },
|
||||
})
|
||||
|
||||
const customDomain = createMockCustomDomain({
|
||||
platformId: mockPlatformOne.id,
|
||||
domain: faker.internet.domainName(),
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('custom_domain')
|
||||
.save(customDomain)
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/custom-domains/${customDomain.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,226 @@
|
||||
import {
|
||||
apId,
|
||||
CreateTemplateRequestBody,
|
||||
PlatformPlan,
|
||||
PlatformRole,
|
||||
PrincipalType,
|
||||
TemplateType,
|
||||
} from '@activepieces/shared'
|
||||
import { FastifyInstance } from 'fastify'
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { initializeDatabase } from '../../../../src/app/database'
|
||||
import { databaseConnection } from '../../../../src/app/database/database-connection'
|
||||
import { setupServer } from '../../../../src/app/server'
|
||||
import { generateMockToken } from '../../../helpers/auth'
|
||||
import {
|
||||
CLOUD_PLATFORM_ID,
|
||||
createMockTemplate,
|
||||
mockAndSaveBasicSetup,
|
||||
mockBasicUser,
|
||||
} from '../../../helpers/mocks'
|
||||
|
||||
let app: FastifyInstance | null = null
|
||||
|
||||
beforeAll(async () => {
|
||||
await initializeDatabase({ runMigrations: false })
|
||||
app = await setupServer()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await databaseConnection().destroy()
|
||||
await app?.close()
|
||||
})
|
||||
|
||||
describe('Templates', () => {
|
||||
describe('List Templates', () => {
|
||||
it('should list platform templates only', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockUser, mockProject, mockPlatformTemplate } =
|
||||
await createMockPlatformTemplate({ platformId: apId(), plan: { manageTemplatesEnabled: true } })
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockUser.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/templates',
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
query: {
|
||||
type: TemplateType.CUSTOM,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(responseBody.data).toHaveLength(1)
|
||||
expect(responseBody.data[0].id).toBe(mockPlatformTemplate.id)
|
||||
})
|
||||
|
||||
it('should list cloud platform template for anonymous users', async () => {
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/templates',
|
||||
query: {
|
||||
type: TemplateType.OFFICIAL,
|
||||
},
|
||||
})
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Create Template', () => {
|
||||
it('should create a flow template', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockOwner, mockProject } = await mockAndSaveBasicSetup({
|
||||
platform: {
|
||||
},
|
||||
plan: {
|
||||
manageTemplatesEnabled: true,
|
||||
},
|
||||
})
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockOwner.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
const mockTemplate = createMockTemplate({
|
||||
platformId: mockPlatform.id,
|
||||
type: TemplateType.CUSTOM,
|
||||
})
|
||||
|
||||
const createTemplateRequest: CreateTemplateRequestBody = {
|
||||
name: mockTemplate.name,
|
||||
description: mockTemplate.description,
|
||||
summary: mockTemplate.summary,
|
||||
flows: mockTemplate.flows,
|
||||
blogUrl: mockTemplate.blogUrl ?? undefined,
|
||||
type: TemplateType.CUSTOM,
|
||||
author: mockTemplate.author,
|
||||
categories: mockTemplate.categories,
|
||||
tags: mockTemplate.tags,
|
||||
metadata: {
|
||||
foo: 'bar',
|
||||
},
|
||||
}
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/templates',
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
body: createTemplateRequest,
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.CREATED)
|
||||
const responseBody = response?.json()
|
||||
expect(responseBody.metadata).toEqual({ foo: 'bar' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Delete Template', () => {
|
||||
it('should not be able delete platform template as member', async () => {
|
||||
// arrange
|
||||
const { mockUser, mockPlatform, mockProject, mockPlatformTemplate } =
|
||||
await createMockPlatformTemplate({ platformId: apId() })
|
||||
const testToken = await generateMockToken({
|
||||
id: mockUser.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/templates/${mockPlatformTemplate.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
|
||||
it('should be able delete platform template as owner', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockOwner, mockProject, mockPlatformTemplate } =
|
||||
await createMockPlatformTemplate({ platformId: apId() })
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockOwner.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/templates/${mockPlatformTemplate.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.NO_CONTENT)
|
||||
})
|
||||
|
||||
it('should not delete platform template when not authenticated', async () => {
|
||||
// arrange
|
||||
const { mockPlatformTemplate } = await createMockPlatformTemplate({
|
||||
platformId: CLOUD_PLATFORM_ID,
|
||||
})
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/templates/${mockPlatformTemplate.id}`,
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
async function createMockPlatformTemplate({ platformId, plan, type }: { platformId: string, plan?: Partial<PlatformPlan>, type?: TemplateType }) {
|
||||
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup({
|
||||
platform: {
|
||||
id: platformId,
|
||||
},
|
||||
plan: {
|
||||
manageTemplatesEnabled: true,
|
||||
...plan,
|
||||
},
|
||||
})
|
||||
|
||||
const mockPlatformTemplate = createMockTemplate({
|
||||
platformId: mockPlatform.id,
|
||||
type: type ?? TemplateType.CUSTOM,
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('template')
|
||||
.save(mockPlatformTemplate)
|
||||
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
return { mockOwner, mockUser, mockPlatform, mockProject, mockPlatformTemplate }
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
import {
|
||||
DefaultProjectRole,
|
||||
FlowOperationType,
|
||||
FlowStatus,
|
||||
FlowTriggerType,
|
||||
PackageType,
|
||||
PieceType,
|
||||
PlatformRole,
|
||||
PrincipalType,
|
||||
ProjectRole,
|
||||
TriggerStrategy,
|
||||
TriggerTestStrategy,
|
||||
} from '@activepieces/shared'
|
||||
import { FastifyInstance } from 'fastify'
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { initializeDatabase } from '../../../../src/app/database'
|
||||
import { databaseConnection } from '../../../../src/app/database/database-connection'
|
||||
import { setupServer } from '../../../../src/app/server'
|
||||
import { generateMockToken } from '../../../helpers/auth'
|
||||
import {
|
||||
createMockFlow,
|
||||
createMockFlowVersion,
|
||||
createMockPieceMetadata,
|
||||
createMockProjectMember,
|
||||
mockAndSaveBasicSetup,
|
||||
mockBasicUser,
|
||||
} from '../../../helpers/mocks'
|
||||
|
||||
let app: FastifyInstance | null = null
|
||||
|
||||
beforeAll(async () => {
|
||||
await initializeDatabase({ runMigrations: false })
|
||||
app = await setupServer()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await databaseConnection().destroy()
|
||||
await app?.close()
|
||||
})
|
||||
|
||||
describe('Flow API', () => {
|
||||
describe('Create Flow endpoint', () => {
|
||||
|
||||
it.each([
|
||||
DefaultProjectRole.ADMIN,
|
||||
DefaultProjectRole.EDITOR,
|
||||
])('Succeeds if user role is %s', async (testRole) => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
|
||||
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: testRole }) as ProjectRole
|
||||
|
||||
const mockProjectMember = createMockProjectMember({
|
||||
userId: mockUser.id,
|
||||
platformId: mockPlatform.id,
|
||||
projectId: mockProject.id,
|
||||
projectRoleId: projectRole.id,
|
||||
})
|
||||
await databaseConnection().getRepository('project_member').save([mockProjectMember])
|
||||
|
||||
const mockToken = await generateMockToken({
|
||||
id: mockUser.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
const mockCreateFlowRequest = {
|
||||
displayName: 'test flow',
|
||||
projectId: mockProject.id,
|
||||
}
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/flows',
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
body: mockCreateFlowRequest,
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.CREATED)
|
||||
})
|
||||
|
||||
it.each([
|
||||
DefaultProjectRole.VIEWER,
|
||||
DefaultProjectRole.OPERATOR,
|
||||
])('Fails if user role is %s', async (testRole) => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
|
||||
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: testRole }) as ProjectRole
|
||||
|
||||
const mockProjectMember = createMockProjectMember({
|
||||
userId: mockUser.id,
|
||||
platformId: mockPlatform.id,
|
||||
projectId: mockProject.id,
|
||||
projectRoleId: projectRole.id,
|
||||
})
|
||||
await databaseConnection().getRepository('project_member').save([mockProjectMember])
|
||||
|
||||
const mockToken = await generateMockToken({
|
||||
id: mockUser.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
const mockCreateFlowRequest = {
|
||||
displayName: 'test flow',
|
||||
projectId: mockProject.id,
|
||||
}
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/flows',
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
body: mockCreateFlowRequest,
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
|
||||
const responseBody = response?.json()
|
||||
expect(responseBody?.code).toBe('PERMISSION_DENIED')
|
||||
expect(responseBody?.params?.userId).toBe(mockUser.id)
|
||||
expect(responseBody?.params?.projectId).toBe(mockProject.id)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Update flow endpoint', () => {
|
||||
it.each([
|
||||
{
|
||||
role: DefaultProjectRole.ADMIN,
|
||||
request: {
|
||||
type: FlowOperationType.CHANGE_STATUS,
|
||||
request: {
|
||||
status: 'ENABLED',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
role: DefaultProjectRole.EDITOR,
|
||||
request: {
|
||||
type: FlowOperationType.CHANGE_STATUS,
|
||||
request: {
|
||||
status: 'ENABLED',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
role: DefaultProjectRole.OPERATOR,
|
||||
request: {
|
||||
type: FlowOperationType.CHANGE_STATUS,
|
||||
request: {
|
||||
status: 'ENABLED',
|
||||
},
|
||||
},
|
||||
},
|
||||
])('Succeeds if user role is %s', async ({ role, request }) => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
|
||||
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: role }) as ProjectRole
|
||||
|
||||
const mockProjectMember = createMockProjectMember({
|
||||
userId: mockUser.id,
|
||||
platformId: mockPlatform.id,
|
||||
projectId: mockProject.id,
|
||||
projectRoleId: projectRole.id,
|
||||
})
|
||||
await databaseConnection().getRepository('project_member').save([mockProjectMember])
|
||||
|
||||
const mockFlow = createMockFlow({
|
||||
projectId: mockProject.id,
|
||||
status: FlowStatus.DISABLED,
|
||||
})
|
||||
await databaseConnection().getRepository('flow').save([mockFlow])
|
||||
const mockPieceMetadata = createMockPieceMetadata({
|
||||
name: '@activepieces/piece-schedule',
|
||||
version: '0.1.5',
|
||||
triggers: {
|
||||
'every_hour': {
|
||||
'name': 'every_hour',
|
||||
'displayName': 'Every Hour',
|
||||
'description': 'Triggers the current flow every hour',
|
||||
'requireAuth': false,
|
||||
'props': {
|
||||
},
|
||||
'type': TriggerStrategy.POLLING,
|
||||
'sampleData': {
|
||||
},
|
||||
'testStrategy': TriggerTestStrategy.TEST_FUNCTION,
|
||||
},
|
||||
},
|
||||
pieceType: PieceType.OFFICIAL,
|
||||
packageType: PackageType.REGISTRY,
|
||||
})
|
||||
await databaseConnection().getRepository('piece_metadata').save([mockPieceMetadata])
|
||||
const mockFlowVersion = createMockFlowVersion({
|
||||
flowId: mockFlow.id,
|
||||
updatedBy: mockUser.id,
|
||||
trigger: {
|
||||
type: FlowTriggerType.PIECE,
|
||||
name: 'trigger',
|
||||
settings: {
|
||||
pieceName: '@activepieces/piece-schedule',
|
||||
pieceVersion: '0.1.5',
|
||||
input: {},
|
||||
propertySettings: {},
|
||||
triggerName: 'every_hour',
|
||||
},
|
||||
valid: true,
|
||||
displayName: 'Trigger',
|
||||
},
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('flow_version')
|
||||
.save([mockFlowVersion])
|
||||
|
||||
await databaseConnection().getRepository('flow').update(mockFlow.id, {
|
||||
publishedVersionId: mockFlowVersion.id,
|
||||
})
|
||||
|
||||
const mockToken = await generateMockToken({
|
||||
id: mockUser.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/flows/${mockFlow.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
body: request,
|
||||
})
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
})
|
||||
|
||||
it.each([
|
||||
{
|
||||
role: DefaultProjectRole.VIEWER,
|
||||
request: {
|
||||
type: FlowOperationType.CHANGE_STATUS,
|
||||
request: {
|
||||
status: 'ENABLED',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
role: DefaultProjectRole.OPERATOR,
|
||||
request: {
|
||||
type: FlowOperationType.CHANGE_NAME,
|
||||
request: {
|
||||
displayName: 'hello',
|
||||
},
|
||||
},
|
||||
},
|
||||
])('Fails if user role is %s', async ({ role, request }) => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
|
||||
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: role }) as ProjectRole
|
||||
|
||||
const mockProjectMember = createMockProjectMember({
|
||||
userId: mockUser.id,
|
||||
platformId: mockPlatform.id,
|
||||
projectId: mockProject.id,
|
||||
projectRoleId: projectRole.id,
|
||||
})
|
||||
await databaseConnection().getRepository('project_member').save([mockProjectMember])
|
||||
|
||||
const mockFlow = createMockFlow({
|
||||
projectId: mockProject.id,
|
||||
status: FlowStatus.DISABLED,
|
||||
})
|
||||
await databaseConnection().getRepository('flow').save([mockFlow])
|
||||
|
||||
const mockFlowVersion = createMockFlowVersion({
|
||||
flowId: mockFlow.id,
|
||||
updatedBy: mockUser.id,
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('flow_version')
|
||||
.save([mockFlowVersion])
|
||||
|
||||
await databaseConnection().getRepository('flow').update(mockFlow.id, {
|
||||
publishedVersionId: mockFlowVersion.id,
|
||||
})
|
||||
|
||||
const mockToken = await generateMockToken({
|
||||
id: mockUser.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/flows/${mockFlow.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
body: request,
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
|
||||
const responseBody = response?.json()
|
||||
expect(responseBody?.code).toBe('PERMISSION_DENIED')
|
||||
expect(responseBody?.params?.userId).toBe(mockUser.id)
|
||||
expect(responseBody?.params?.projectId).toBe(mockProject.id)
|
||||
})
|
||||
})
|
||||
|
||||
describe('List Flows endpoint', () => {
|
||||
it.each([
|
||||
DefaultProjectRole.ADMIN,
|
||||
DefaultProjectRole.EDITOR,
|
||||
DefaultProjectRole.OPERATOR,
|
||||
DefaultProjectRole.VIEWER,
|
||||
])('Succeeds if user role is %s', async (testRole) => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
|
||||
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: testRole }) as ProjectRole
|
||||
|
||||
const mockProjectMember = createMockProjectMember({
|
||||
userId: mockUser.id,
|
||||
platformId: mockPlatform.id,
|
||||
projectId: mockProject.id,
|
||||
projectRoleId: projectRole.id,
|
||||
})
|
||||
await databaseConnection().getRepository('project_member').save([mockProjectMember])
|
||||
|
||||
const mockToken = await generateMockToken({
|
||||
id: mockUser.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/flows',
|
||||
query: {
|
||||
projectId: mockProject.id,
|
||||
status: 'ENABLED',
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,333 @@
|
||||
import { GitBranchType } from '@activepieces/ee-shared'
|
||||
import { PrincipalType } from '@activepieces/shared'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { FastifyInstance } from 'fastify'
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { initializeDatabase } from '../../../../src/app/database'
|
||||
import { databaseConnection } from '../../../../src/app/database/database-connection'
|
||||
import { setupServer } from '../../../../src/app/server'
|
||||
import { generateMockToken } from '../../../helpers/auth'
|
||||
import {
|
||||
createMockGitRepo,
|
||||
createMockProject,
|
||||
mockAndSaveBasicSetup,
|
||||
mockAndSaveBasicSetupWithApiKey,
|
||||
mockBasicUser,
|
||||
} from '../../../helpers/mocks'
|
||||
|
||||
let app: FastifyInstance | null = null
|
||||
|
||||
beforeAll(async () => {
|
||||
await initializeDatabase({ runMigrations: false })
|
||||
app = await setupServer()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await databaseConnection().destroy()
|
||||
await app?.close()
|
||||
})
|
||||
|
||||
describe('Git API', () => {
|
||||
describe('Create API', () => {
|
||||
it('should not allow create git repo for other projects', async () => {
|
||||
const { mockPlatform, mockProject, mockOwner } = await mockAndSaveBasicSetup({
|
||||
platform: {
|
||||
},
|
||||
plan: {
|
||||
environmentsEnabled: true,
|
||||
},
|
||||
})
|
||||
|
||||
const { mockUser: mockUser2 } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
const mockProject2 = createMockProject({ platformId: mockPlatform.id, ownerId: mockUser2.id })
|
||||
await databaseConnection().getRepository('project').save(mockProject2)
|
||||
|
||||
const request = {
|
||||
projectId: mockProject2.id,
|
||||
remoteUrl: `git@${faker.internet.url()}`,
|
||||
sshPrivateKey: faker.hacker.noun(),
|
||||
branch: 'main',
|
||||
branchType: GitBranchType.PRODUCTION,
|
||||
slug: 'test-slug',
|
||||
}
|
||||
|
||||
const token = await generateMockToken({
|
||||
id: mockOwner.id,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
type: PrincipalType.USER,
|
||||
})
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/git-repos',
|
||||
payload: request,
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
|
||||
it('should create a git repo', async () => {
|
||||
const { mockProject, mockOwner } = await mockAndSaveBasicSetup({
|
||||
platform: {
|
||||
},
|
||||
plan: {
|
||||
environmentsEnabled: true,
|
||||
},
|
||||
})
|
||||
|
||||
const request = {
|
||||
projectId: mockProject.id,
|
||||
remoteUrl: `git@${faker.internet.url()}`,
|
||||
sshPrivateKey: faker.hacker.noun(),
|
||||
branch: 'main',
|
||||
branchType: GitBranchType.PRODUCTION,
|
||||
slug: 'test-slug',
|
||||
}
|
||||
const token = await generateMockToken({
|
||||
id: mockOwner.id,
|
||||
projectId: mockProject.id,
|
||||
type: PrincipalType.USER,
|
||||
platform: {
|
||||
id: mockProject.platformId,
|
||||
},
|
||||
})
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/git-repos',
|
||||
payload: request,
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
const responseBody = response?.json()
|
||||
expect(response?.statusCode).toBe(StatusCodes.CREATED)
|
||||
expect(responseBody.sshPrivateKey).toBeUndefined()
|
||||
expect(responseBody.remoteUrl).toBe(request.remoteUrl)
|
||||
expect(responseBody.branch).toBe(request.branch)
|
||||
expect(responseBody.created).toBeDefined()
|
||||
expect(responseBody.updated).toBeDefined()
|
||||
expect(responseBody.id).toBeDefined()
|
||||
expect(responseBody.projectId).toBe(mockProject.id)
|
||||
expect(responseBody.slug).toBe('test-slug')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Delete API', () => {
|
||||
it('should delete a git repo', async () => {
|
||||
const { mockProject, mockOwner } = await mockAndSaveBasicSetup({
|
||||
platform: {
|
||||
},
|
||||
plan: {
|
||||
environmentsEnabled: true,
|
||||
},
|
||||
})
|
||||
|
||||
const mockGitRepo = createMockGitRepo({ projectId: mockProject.id })
|
||||
await databaseConnection().getRepository('git_repo').save(mockGitRepo)
|
||||
|
||||
const token = await generateMockToken({
|
||||
id: mockOwner.id,
|
||||
projectId: mockProject.id,
|
||||
type: PrincipalType.USER,
|
||||
platform: {
|
||||
id: mockProject.platformId,
|
||||
},
|
||||
})
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: '/v1/git-repos/' + mockGitRepo.id,
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
expect(response?.statusCode).toBe(StatusCodes.NO_CONTENT)
|
||||
})
|
||||
it('should not allow delete git repo for other projects', async () => {
|
||||
const { mockPlatform, mockProject, mockOwner } = await mockAndSaveBasicSetup({
|
||||
platform: {
|
||||
},
|
||||
plan: {
|
||||
environmentsEnabled: true,
|
||||
},
|
||||
})
|
||||
|
||||
const mockProject2 = createMockProject({ platformId: mockPlatform.id, ownerId: mockOwner.id })
|
||||
await databaseConnection().getRepository('project').save(mockProject2)
|
||||
|
||||
const mockGitRepo = createMockGitRepo({ projectId: mockProject.id })
|
||||
const mockGitRepo2 = createMockGitRepo({ projectId: mockProject2.id })
|
||||
await databaseConnection()
|
||||
.getRepository('git_repo')
|
||||
.save([mockGitRepo, mockGitRepo2])
|
||||
|
||||
const token = await generateMockToken({
|
||||
id: mockOwner.id,
|
||||
projectId: mockProject.id,
|
||||
type: PrincipalType.USER,
|
||||
platform: {
|
||||
id: mockProject.platformId,
|
||||
},
|
||||
})
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: '/v1/git-repos/' + mockGitRepo2.id,
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.NOT_FOUND)
|
||||
})
|
||||
})
|
||||
|
||||
describe('List API', () => {
|
||||
it('should list return forbidden when api request wrong project', async () => {
|
||||
const { mockPlatform, mockProject, mockApiKey, mockOwner } = await mockAndSaveBasicSetupWithApiKey({
|
||||
platform: {
|
||||
},
|
||||
plan: {
|
||||
environmentsEnabled: true,
|
||||
},
|
||||
})
|
||||
const { mockProject: mockProject3 } = await mockAndSaveBasicSetup({
|
||||
platform: {
|
||||
},
|
||||
plan: {
|
||||
environmentsEnabled: true,
|
||||
},
|
||||
})
|
||||
|
||||
const mockProject2 = createMockProject({ platformId: mockPlatform.id, ownerId: mockOwner.id })
|
||||
await databaseConnection()
|
||||
.getRepository('project')
|
||||
.save([mockProject2])
|
||||
|
||||
const mockGitRepo = createMockGitRepo({ projectId: mockProject.id })
|
||||
const mockGitRepo2 = createMockGitRepo({ projectId: mockProject2.id })
|
||||
await databaseConnection()
|
||||
.getRepository('git_repo')
|
||||
.save([mockGitRepo, mockGitRepo2])
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/git-repos?projectId=' + mockProject3.id,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockApiKey.value}`,
|
||||
},
|
||||
})
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
it('should list return forbidden when user request wrong project', async () => {
|
||||
const { mockPlatform, mockProject, mockOwner } = await mockAndSaveBasicSetup({
|
||||
platform: {
|
||||
},
|
||||
plan: {
|
||||
environmentsEnabled: true,
|
||||
},
|
||||
})
|
||||
const { mockProject: mockProject3 } = await mockAndSaveBasicSetup({
|
||||
platform: {
|
||||
},
|
||||
plan: {
|
||||
environmentsEnabled: true,
|
||||
},
|
||||
})
|
||||
|
||||
const mockProject2 = createMockProject({ platformId: mockPlatform.id, ownerId: mockOwner.id })
|
||||
await databaseConnection()
|
||||
.getRepository('project')
|
||||
.save([mockProject2])
|
||||
|
||||
const mockGitRepo = createMockGitRepo({ projectId: mockProject.id })
|
||||
const mockGitRepo2 = createMockGitRepo({ projectId: mockProject2.id })
|
||||
await databaseConnection()
|
||||
.getRepository('git_repo')
|
||||
.save([mockGitRepo, mockGitRepo2])
|
||||
|
||||
const token = await generateMockToken({
|
||||
id: mockOwner.id,
|
||||
projectId: mockProject.id,
|
||||
type: PrincipalType.USER,
|
||||
platform: {
|
||||
id: mockProject.platformId,
|
||||
},
|
||||
})
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/git-repos?projectId=' + mockProject3.id,
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
it('should list a git repo', async () => {
|
||||
const { mockPlatform, mockProject, mockOwner } = await mockAndSaveBasicSetup({
|
||||
platform: {
|
||||
},
|
||||
plan: {
|
||||
environmentsEnabled: true,
|
||||
},
|
||||
})
|
||||
|
||||
const mockProject2 = createMockProject({ platformId: mockPlatform.id, ownerId: mockOwner.id })
|
||||
await databaseConnection()
|
||||
.getRepository('project')
|
||||
.save([mockProject2])
|
||||
|
||||
const mockGitRepo = createMockGitRepo({ projectId: mockProject.id })
|
||||
const mockGitRepo2 = createMockGitRepo({ projectId: mockProject2.id })
|
||||
await databaseConnection()
|
||||
.getRepository('git_repo')
|
||||
.save([mockGitRepo, mockGitRepo2])
|
||||
|
||||
const token = await generateMockToken({
|
||||
id: mockOwner.id,
|
||||
projectId: mockProject.id,
|
||||
type: PrincipalType.USER,
|
||||
platform: {
|
||||
id: mockProject.platformId,
|
||||
},
|
||||
})
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/git-repos?projectId=' + mockProject.id,
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
const responseBody = response?.json()
|
||||
expect(responseBody.data.length).toBe(1)
|
||||
|
||||
const gitRepo = responseBody.data[0]
|
||||
expect(gitRepo.sshPrivateKey).toBeUndefined()
|
||||
expect(gitRepo.remoteUrl).toBe(mockGitRepo.remoteUrl)
|
||||
expect(gitRepo.branch).toBe(mockGitRepo.branch)
|
||||
expect(gitRepo.created).toBeDefined()
|
||||
expect(gitRepo.updated).toBeDefined()
|
||||
expect(gitRepo.id).toBeDefined()
|
||||
expect(gitRepo.projectId).toBe(mockProject.id)
|
||||
expect(gitRepo.slug).toBe(mockGitRepo.slug)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,606 @@
|
||||
import {
|
||||
apId,
|
||||
AppConnectionScope,
|
||||
AppConnectionType,
|
||||
PackageType,
|
||||
PlatformRole,
|
||||
PrincipalType,
|
||||
UpdateGlobalConnectionValueRequestBody,
|
||||
UpsertGlobalConnectionRequestBody,
|
||||
} from '@activepieces/shared'
|
||||
import { FastifyInstance } from 'fastify'
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { databaseConnection } from '../../../../src/app/database/database-connection'
|
||||
import { setupServer } from '../../../../src/app/server'
|
||||
import { generateMockToken } from '../../../helpers/auth'
|
||||
import {
|
||||
createMockPieceMetadata,
|
||||
mockAndSaveBasicSetup,
|
||||
mockBasicUser,
|
||||
} from '../../../helpers/mocks'
|
||||
|
||||
let app: FastifyInstance | null = null
|
||||
|
||||
beforeAll(async () => {
|
||||
await databaseConnection().initialize()
|
||||
app = await setupServer()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await databaseConnection().destroy()
|
||||
await app?.close()
|
||||
})
|
||||
|
||||
const setupWithGlobalConnections = () => {
|
||||
return mockAndSaveBasicSetup({
|
||||
platform: {
|
||||
},
|
||||
plan: {
|
||||
globalConnectionsEnabled: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('GlobalConnection API', () => {
|
||||
describe('Upsert GlobalConnection endpoint', () => {
|
||||
it('Succeeds if user is platform owner', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject, mockOwner } = await setupWithGlobalConnections()
|
||||
|
||||
const mockPieceMetadata = createMockPieceMetadata({
|
||||
projectId: mockProject.id,
|
||||
platformId: mockPlatform.id,
|
||||
packageType: PackageType.REGISTRY,
|
||||
})
|
||||
await databaseConnection().getRepository('piece_metadata').save([mockPieceMetadata])
|
||||
|
||||
|
||||
|
||||
const mockToken = await generateMockToken({
|
||||
id: mockOwner.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
const mockUpsertGlobalConnectionRequest: UpsertGlobalConnectionRequestBody = {
|
||||
pieceVersion: mockPieceMetadata.version,
|
||||
displayName: 'test global connection',
|
||||
pieceName: mockPieceMetadata.name,
|
||||
projectIds: [mockProject.id],
|
||||
scope: AppConnectionScope.PLATFORM,
|
||||
type: AppConnectionType.SECRET_TEXT,
|
||||
value: {
|
||||
type: AppConnectionType.SECRET_TEXT,
|
||||
secret_text: 'test-secret-text',
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/global-connections',
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
body: mockUpsertGlobalConnectionRequest,
|
||||
})
|
||||
const responseBody = response?.json()
|
||||
expect(responseBody.pieceVersion).toEqual(mockPieceMetadata.version)
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.CREATED)
|
||||
})
|
||||
|
||||
it('Fails if user is not platform owner', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject } = await setupWithGlobalConnections()
|
||||
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
const mockPieceMetadata = createMockPieceMetadata({
|
||||
projectId: mockProject.id,
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
await databaseConnection().getRepository('piece_metadata').save([mockPieceMetadata])
|
||||
|
||||
|
||||
|
||||
const mockToken = await generateMockToken({
|
||||
id: mockUser.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
const mockUpsertGlobalConnectionRequest: UpsertGlobalConnectionRequestBody = {
|
||||
pieceVersion: mockPieceMetadata.version,
|
||||
displayName: 'test global connection',
|
||||
pieceName: mockPieceMetadata.name,
|
||||
scope: AppConnectionScope.PLATFORM,
|
||||
projectIds: [mockProject.id],
|
||||
type: AppConnectionType.SECRET_TEXT,
|
||||
value: {
|
||||
type: AppConnectionType.SECRET_TEXT,
|
||||
secret_text: 'test-secret-text',
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/global-connections',
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
body: mockUpsertGlobalConnectionRequest,
|
||||
})
|
||||
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
|
||||
it('Fails if project ids are invalid', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject, mockOwner } = await setupWithGlobalConnections()
|
||||
|
||||
const mockPieceMetadata = createMockPieceMetadata({
|
||||
projectId: mockProject.id,
|
||||
platformId: mockPlatform.id,
|
||||
packageType: PackageType.REGISTRY,
|
||||
})
|
||||
await databaseConnection().getRepository('piece_metadata').save([mockPieceMetadata])
|
||||
|
||||
|
||||
|
||||
const mockToken = await generateMockToken({
|
||||
id: mockOwner.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
const mockUpsertGlobalConnectionRequest: UpsertGlobalConnectionRequestBody = {
|
||||
pieceVersion: mockPieceMetadata.version,
|
||||
displayName: 'test global connection',
|
||||
pieceName: mockPieceMetadata.name,
|
||||
projectIds: [apId()], // Invalid project ID
|
||||
scope: AppConnectionScope.PLATFORM,
|
||||
type: AppConnectionType.SECRET_TEXT,
|
||||
value: {
|
||||
type: AppConnectionType.SECRET_TEXT,
|
||||
secret_text: 'test-secret-text',
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/global-connections',
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
body: mockUpsertGlobalConnectionRequest,
|
||||
})
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.NOT_FOUND)
|
||||
})
|
||||
})
|
||||
|
||||
describe('List GlobalConnections endpoint', () => {
|
||||
it('Succeeds if user is platform owner', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject, mockOwner } = await setupWithGlobalConnections()
|
||||
|
||||
const mockToken = await generateMockToken({
|
||||
id: mockOwner.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/global-connections',
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
})
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
})
|
||||
|
||||
it('Fails if user is not platform owner', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject } = await setupWithGlobalConnections()
|
||||
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
const mockToken = await generateMockToken({
|
||||
id: mockUser.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/global-connections',
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Delete GlobalConnection endpoint', () => {
|
||||
it('Succeeds if user is platform owner', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject, mockOwner } = await setupWithGlobalConnections()
|
||||
|
||||
const mockPieceMetadata = createMockPieceMetadata({
|
||||
projectId: mockProject.id,
|
||||
platformId: mockPlatform.id,
|
||||
packageType: PackageType.REGISTRY,
|
||||
})
|
||||
await databaseConnection().getRepository('piece_metadata').save([mockPieceMetadata])
|
||||
|
||||
const mockToken = await generateMockToken({
|
||||
id: mockOwner.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
const mockUpsertGlobalConnectionRequest: UpsertGlobalConnectionRequestBody = {
|
||||
pieceVersion: mockPieceMetadata.version,
|
||||
displayName: 'test global connection',
|
||||
pieceName: mockPieceMetadata.name,
|
||||
scope: AppConnectionScope.PLATFORM,
|
||||
projectIds: [mockProject.id],
|
||||
type: AppConnectionType.SECRET_TEXT,
|
||||
value: {
|
||||
type: AppConnectionType.SECRET_TEXT,
|
||||
secret_text: 'test-secret-text',
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
const upsertResponse = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/global-connections',
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
body: mockUpsertGlobalConnectionRequest,
|
||||
})
|
||||
const connectionId = upsertResponse?.json().id
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/global-connections/${connectionId}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
})
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.NO_CONTENT)
|
||||
})
|
||||
|
||||
it('Fails if user is not platform owner', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject, mockOwner } = await setupWithGlobalConnections()
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
const mockPieceMetadata = createMockPieceMetadata({
|
||||
projectId: mockProject.id,
|
||||
platformId: mockPlatform.id,
|
||||
packageType: PackageType.REGISTRY,
|
||||
})
|
||||
await databaseConnection().getRepository('piece_metadata').save([mockPieceMetadata])
|
||||
|
||||
|
||||
|
||||
const mockOwnerToken = await generateMockToken({
|
||||
id: mockOwner.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
const mockUpsertGlobalConnectionRequest: UpsertGlobalConnectionRequestBody = {
|
||||
pieceVersion: mockPieceMetadata.version,
|
||||
displayName: 'test global connection',
|
||||
pieceName: mockPieceMetadata.name,
|
||||
scope: AppConnectionScope.PLATFORM,
|
||||
projectIds: [mockProject.id],
|
||||
type: AppConnectionType.SECRET_TEXT,
|
||||
value: {
|
||||
type: AppConnectionType.SECRET_TEXT,
|
||||
secret_text: 'test-secret-text',
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
const upsertResponse = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/global-connections',
|
||||
headers: {
|
||||
authorization: `Bearer ${mockOwnerToken}`,
|
||||
},
|
||||
body: mockUpsertGlobalConnectionRequest,
|
||||
})
|
||||
const connectionId = upsertResponse?.json().id
|
||||
|
||||
const mockUserToken = await generateMockToken({
|
||||
id: mockUser.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/global-connections/${connectionId}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockUserToken}`,
|
||||
},
|
||||
})
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Update GlobalConnection endpoint', () => {
|
||||
it('Succeeds if user is platform owner', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject, mockOwner } = await setupWithGlobalConnections()
|
||||
|
||||
const mockPieceMetadata = createMockPieceMetadata({
|
||||
projectId: mockProject.id,
|
||||
platformId: mockPlatform.id,
|
||||
packageType: PackageType.REGISTRY,
|
||||
})
|
||||
await databaseConnection().getRepository('piece_metadata').save([mockPieceMetadata])
|
||||
|
||||
|
||||
|
||||
const mockToken = await generateMockToken({
|
||||
id: mockOwner.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
const mockUpsertGlobalConnectionRequest: UpsertGlobalConnectionRequestBody = {
|
||||
pieceVersion: mockPieceMetadata.version,
|
||||
displayName: 'test global connection',
|
||||
pieceName: mockPieceMetadata.name,
|
||||
scope: AppConnectionScope.PLATFORM,
|
||||
type: AppConnectionType.SECRET_TEXT,
|
||||
projectIds: [mockProject.id],
|
||||
value: {
|
||||
type: AppConnectionType.SECRET_TEXT,
|
||||
secret_text: 'test-secret-text',
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
const upsertResponse = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/global-connections',
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
body: mockUpsertGlobalConnectionRequest,
|
||||
})
|
||||
|
||||
const connectionId = upsertResponse?.json().id
|
||||
const mockUpdateGlobalConnectionRequest: UpdateGlobalConnectionValueRequestBody = {
|
||||
displayName: 'updated-global-connection',
|
||||
}
|
||||
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/global-connections/${connectionId}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
body: mockUpdateGlobalConnectionRequest,
|
||||
})
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(response?.json().displayName).toBe('updated-global-connection')
|
||||
})
|
||||
|
||||
it('Fails if user is not platform owner', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject, mockOwner } = await setupWithGlobalConnections()
|
||||
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
|
||||
const mockPieceMetadata = createMockPieceMetadata({
|
||||
projectId: mockProject.id,
|
||||
platformId: mockPlatform.id,
|
||||
packageType: PackageType.REGISTRY,
|
||||
})
|
||||
await databaseConnection().getRepository('piece_metadata').save([mockPieceMetadata])
|
||||
|
||||
|
||||
|
||||
const mockOwnerToken = await generateMockToken({
|
||||
id: mockOwner.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
const mockUpsertGlobalConnectionRequest: UpsertGlobalConnectionRequestBody = {
|
||||
pieceVersion: mockPieceMetadata.version,
|
||||
displayName: 'test global connection',
|
||||
pieceName: mockPieceMetadata.name,
|
||||
scope: AppConnectionScope.PLATFORM,
|
||||
type: AppConnectionType.SECRET_TEXT,
|
||||
projectIds: [mockProject.id],
|
||||
value: {
|
||||
type: AppConnectionType.SECRET_TEXT,
|
||||
secret_text: 'test-secret-text',
|
||||
},
|
||||
|
||||
|
||||
}
|
||||
|
||||
const upsertResponse = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/global-connections',
|
||||
headers: {
|
||||
authorization: `Bearer ${mockOwnerToken}`,
|
||||
},
|
||||
body: mockUpsertGlobalConnectionRequest,
|
||||
})
|
||||
|
||||
const connectionId = upsertResponse?.json().id
|
||||
const mockUserToken = await generateMockToken({
|
||||
id: mockUser.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
const mockUpdateGlobalConnectionRequest = {
|
||||
displayName: 'updated-global-connection',
|
||||
}
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/global-connections/${connectionId}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockUserToken}`,
|
||||
},
|
||||
body: mockUpdateGlobalConnectionRequest,
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
|
||||
it('Fails if project ids are invalid', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject, mockOwner } = await setupWithGlobalConnections()
|
||||
|
||||
const mockPieceMetadata = createMockPieceMetadata({
|
||||
projectId: mockProject.id,
|
||||
platformId: mockPlatform.id,
|
||||
packageType: PackageType.REGISTRY,
|
||||
})
|
||||
await databaseConnection().getRepository('piece_metadata').save([mockPieceMetadata])
|
||||
|
||||
|
||||
|
||||
const mockToken = await generateMockToken({
|
||||
id: mockOwner.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
const mockUpsertGlobalConnectionRequest: UpsertGlobalConnectionRequestBody = {
|
||||
pieceVersion: mockPieceMetadata.version,
|
||||
displayName: 'test global connection',
|
||||
pieceName: mockPieceMetadata.name,
|
||||
scope: AppConnectionScope.PLATFORM,
|
||||
type: AppConnectionType.SECRET_TEXT,
|
||||
projectIds: [mockProject.id],
|
||||
value: {
|
||||
type: AppConnectionType.SECRET_TEXT,
|
||||
secret_text: 'test-secret-text',
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
const upsertResponse = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/global-connections',
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
body: mockUpsertGlobalConnectionRequest,
|
||||
})
|
||||
|
||||
const connectionId = upsertResponse?.json().id
|
||||
|
||||
const mockUpdateGlobalConnectionRequest: UpdateGlobalConnectionValueRequestBody = {
|
||||
projectIds: [apId()], // Invalid project ID
|
||||
displayName: 'updated-global-connection',
|
||||
}
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/global-connections/${connectionId}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
body: mockUpdateGlobalConnectionRequest,
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.NOT_FOUND)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,365 @@
|
||||
import { apId, DefaultProjectRole, PiecesFilterType, PieceType, ProjectRole } from '@activepieces/shared'
|
||||
import { FastifyInstance } from 'fastify'
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { initializeDatabase } from '../../../../src/app/database'
|
||||
import { databaseConnection } from '../../../../src/app/database/database-connection'
|
||||
import { setupServer } from '../../../../src/app/server'
|
||||
import { generateMockExternalToken } from '../../../helpers/auth'
|
||||
import {
|
||||
createMockPieceMetadata,
|
||||
createMockPieceTag,
|
||||
createMockProject,
|
||||
createMockSigningKey,
|
||||
createMockTag,
|
||||
mockAndSaveBasicSetup,
|
||||
mockBasicUser,
|
||||
} from '../../../helpers/mocks'
|
||||
|
||||
let app: FastifyInstance | null = null
|
||||
|
||||
beforeAll(async () => {
|
||||
await initializeDatabase({ runMigrations: false })
|
||||
app = await setupServer()
|
||||
})
|
||||
|
||||
|
||||
afterAll(async () => {
|
||||
await databaseConnection().destroy()
|
||||
await app?.close()
|
||||
})
|
||||
|
||||
|
||||
describe('Managed Authentication API', () => {
|
||||
describe('External token endpoint', () => {
|
||||
it('Signs up new users', async () => {
|
||||
// arrange
|
||||
const { mockPlatform } = await mockAndSaveBasicSetup()
|
||||
|
||||
const mockSigningKey = createMockSigningKey({
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('signing_key')
|
||||
.save(mockSigningKey)
|
||||
|
||||
const { mockExternalToken, mockExternalTokenPayload } = generateMockExternalToken({
|
||||
platformId: mockPlatform.id,
|
||||
signingKeyId: mockSigningKey.id,
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/managed-authn/external-token',
|
||||
body: {
|
||||
externalAccessToken: mockExternalToken,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(responseBody?.id).toHaveLength(21)
|
||||
expect(responseBody?.firstName).toBe(mockExternalTokenPayload.firstName)
|
||||
expect(responseBody?.lastName).toBe(mockExternalTokenPayload.lastName)
|
||||
expect(responseBody?.trackEvents).toBe(true)
|
||||
expect(responseBody?.newsLetter).toBe(false)
|
||||
expect(responseBody?.password).toBeUndefined()
|
||||
expect(responseBody?.status).toBe('ACTIVE')
|
||||
expect(responseBody?.verified).toBe(true)
|
||||
expect(responseBody?.externalId).toBe(
|
||||
mockExternalTokenPayload.externalUserId,
|
||||
)
|
||||
expect(responseBody?.platformId).toBe(mockPlatform.id)
|
||||
expect(responseBody?.projectId).toHaveLength(21)
|
||||
expect(responseBody?.token).toBeDefined()
|
||||
})
|
||||
|
||||
it('Creates new project', async () => {
|
||||
// arrange
|
||||
const { mockPlatform } = await mockAndSaveBasicSetup()
|
||||
|
||||
const mockSigningKey = createMockSigningKey({
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('signing_key')
|
||||
.save(mockSigningKey)
|
||||
|
||||
const { mockExternalToken, mockExternalTokenPayload } =
|
||||
generateMockExternalToken({
|
||||
platformId: mockPlatform.id,
|
||||
signingKeyId: mockSigningKey.id,
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/managed-authn/external-token',
|
||||
body: {
|
||||
externalAccessToken: mockExternalToken,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
|
||||
const generatedProject = await databaseConnection()
|
||||
.getRepository('project')
|
||||
.findOneBy({
|
||||
id: responseBody?.projectId,
|
||||
})
|
||||
|
||||
expect(generatedProject?.displayName).toBe(
|
||||
mockExternalTokenPayload.externalProjectId,
|
||||
)
|
||||
expect(generatedProject?.ownerId).toBe(mockPlatform.ownerId)
|
||||
expect(generatedProject?.platformId).toBe(mockPlatform.id)
|
||||
expect(generatedProject?.externalId).toBe(
|
||||
mockExternalTokenPayload.externalProjectId,
|
||||
)
|
||||
})
|
||||
|
||||
it('Sync Pieces when exchanging external token', async () => {
|
||||
// arrange
|
||||
const { mockPlatform } = await mockAndSaveBasicSetup()
|
||||
|
||||
const mockPieceMetadata1 = createMockPieceMetadata({
|
||||
name: '@ap/a',
|
||||
version: '0.0.1',
|
||||
pieceType: PieceType.OFFICIAL,
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('piece_metadata')
|
||||
.save(mockPieceMetadata1)
|
||||
|
||||
const mockTag = createMockTag({
|
||||
id: apId(),
|
||||
platformId: mockPlatform.id,
|
||||
name: 'free',
|
||||
})
|
||||
|
||||
await databaseConnection()
|
||||
.getRepository('tag')
|
||||
.save(mockTag)
|
||||
|
||||
|
||||
const mockPieceTag = createMockPieceTag({
|
||||
platformId: mockPlatform.id,
|
||||
tagId: mockTag.id,
|
||||
pieceName: '@ap/a',
|
||||
})
|
||||
|
||||
await databaseConnection()
|
||||
.getRepository('piece_tag')
|
||||
.save(mockPieceTag)
|
||||
|
||||
|
||||
const mockSigningKey = createMockSigningKey({
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('signing_key')
|
||||
.save(mockSigningKey)
|
||||
|
||||
|
||||
|
||||
const { mockExternalToken } = generateMockExternalToken({
|
||||
platformId: mockPlatform.id,
|
||||
signingKeyId: mockSigningKey.id,
|
||||
pieces: {
|
||||
filterType: PiecesFilterType.ALLOWED,
|
||||
tags: ['free'],
|
||||
},
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/managed-authn/external-token',
|
||||
body: {
|
||||
externalAccessToken: mockExternalToken,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
|
||||
const generatedProject = await databaseConnection()
|
||||
.getRepository('project_plan')
|
||||
.findOneBy({ projectId: responseBody?.projectId })
|
||||
|
||||
expect(generatedProject?.piecesFilterType).toBe('ALLOWED')
|
||||
expect(generatedProject?.pieces).toStrictEqual(['@ap/a'])
|
||||
})
|
||||
|
||||
it('Adds new user as a member in new project', async () => {
|
||||
// arrange
|
||||
const { mockPlatform } = await mockAndSaveBasicSetup()
|
||||
|
||||
const mockSigningKey = createMockSigningKey({
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('signing_key')
|
||||
.save(mockSigningKey)
|
||||
|
||||
|
||||
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.VIEWER }) as ProjectRole
|
||||
|
||||
const { mockExternalToken } = generateMockExternalToken({
|
||||
platformId: mockPlatform.id,
|
||||
signingKeyId: mockSigningKey.id,
|
||||
projectRole: projectRole.name,
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/managed-authn/external-token',
|
||||
body: {
|
||||
externalAccessToken: mockExternalToken,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
|
||||
const generatedProjectMember = await databaseConnection()
|
||||
.getRepository('project_member')
|
||||
.findOneBy({
|
||||
projectId: responseBody?.projectId,
|
||||
userId: responseBody?.id,
|
||||
})
|
||||
|
||||
expect(generatedProjectMember?.projectId).toBe(responseBody?.projectId)
|
||||
expect(generatedProjectMember?.userId).toBe(responseBody?.id)
|
||||
expect(generatedProjectMember?.platformId).toBe(mockPlatform.id)
|
||||
expect(generatedProjectMember?.projectRoleId).toBe(projectRole.id)
|
||||
})
|
||||
|
||||
it('Adds new user to existing project', async () => {
|
||||
// arrange
|
||||
const { mockOwner, mockPlatform } = await mockAndSaveBasicSetup()
|
||||
|
||||
const mockSigningKey = createMockSigningKey({
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('signing_key')
|
||||
.save(mockSigningKey)
|
||||
|
||||
const mockExternalProjectId = apId()
|
||||
|
||||
const mockProject = createMockProject({
|
||||
ownerId: mockOwner.id,
|
||||
platformId: mockPlatform.id,
|
||||
externalId: mockExternalProjectId,
|
||||
})
|
||||
await databaseConnection().getRepository('project').save(mockProject)
|
||||
|
||||
const { mockExternalToken } = generateMockExternalToken({
|
||||
platformId: mockPlatform.id,
|
||||
signingKeyId: mockSigningKey.id,
|
||||
externalProjectId: mockExternalProjectId,
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/managed-authn/external-token',
|
||||
body: {
|
||||
externalAccessToken: mockExternalToken,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(responseBody?.projectId).toBe(mockProject.id)
|
||||
})
|
||||
|
||||
it('Signs in existing users', async () => {
|
||||
// arrange
|
||||
const { mockOwner, mockPlatform } = await mockAndSaveBasicSetup()
|
||||
|
||||
const mockSigningKey = createMockSigningKey({
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('signing_key')
|
||||
.save(mockSigningKey)
|
||||
|
||||
const { mockExternalToken, mockExternalTokenPayload } = generateMockExternalToken({
|
||||
platformId: mockPlatform.id,
|
||||
signingKeyId: mockSigningKey.id,
|
||||
})
|
||||
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
externalId: mockExternalTokenPayload.externalUserId,
|
||||
platformId: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
const mockProject = createMockProject({
|
||||
ownerId: mockOwner.id,
|
||||
platformId: mockPlatform.id,
|
||||
externalId: mockExternalTokenPayload.externalProjectId,
|
||||
})
|
||||
await databaseConnection().getRepository('project').save(mockProject)
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/managed-authn/external-token',
|
||||
body: {
|
||||
externalAccessToken: mockExternalToken,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(responseBody?.projectId).toBe(mockProject.id)
|
||||
expect(responseBody?.id).toBe(mockUser.id)
|
||||
})
|
||||
|
||||
it('Fails if signing key is not found', async () => {
|
||||
// arrange
|
||||
await mockAndSaveBasicSetup()
|
||||
|
||||
const nonExistentSigningKeyId = apId()
|
||||
|
||||
const { mockExternalToken } = generateMockExternalToken({
|
||||
signingKeyId: nonExistentSigningKeyId,
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/managed-authn/external-token',
|
||||
body: {
|
||||
externalAccessToken: mockExternalToken,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.UNAUTHORIZED)
|
||||
expect(responseBody?.params?.message).toBe(
|
||||
`signing key not found signingKeyId=${nonExistentSigningKeyId}`,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,237 @@
|
||||
import { UpsertOAuth2AppRequest } from '@activepieces/ee-shared'
|
||||
import { PlatformRole, PrincipalType } from '@activepieces/shared'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { FastifyInstance } from 'fastify'
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { initializeDatabase } from '../../../../src/app/database'
|
||||
import { databaseConnection } from '../../../../src/app/database/database-connection'
|
||||
import { setupServer } from '../../../../src/app/server'
|
||||
import { generateMockToken } from '../../../helpers/auth'
|
||||
import {
|
||||
createMockOAuthApp,
|
||||
mockAndSaveBasicSetup,
|
||||
mockBasicUser,
|
||||
} from '../../../helpers/mocks'
|
||||
|
||||
let app: FastifyInstance | null = null
|
||||
|
||||
const upsertRequest: UpsertOAuth2AppRequest = {
|
||||
pieceName: faker.lorem.word(),
|
||||
clientId: faker.lorem.word(),
|
||||
clientSecret: faker.lorem.word(),
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
await initializeDatabase({ runMigrations: false })
|
||||
app = await setupServer()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await databaseConnection().destroy()
|
||||
await app?.close()
|
||||
})
|
||||
|
||||
describe('OAuth App API', () => {
|
||||
describe('Upsert OAuth APP API', () => {
|
||||
it('new OAuth App', async () => {
|
||||
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockOwner.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/oauth-apps',
|
||||
body: upsertRequest,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(responseBody.id).toHaveLength(21)
|
||||
expect(responseBody.platformId).toBe(mockPlatform.id)
|
||||
expect(responseBody.pieceName).toBe(upsertRequest.pieceName)
|
||||
expect(responseBody.clientId).toBe(upsertRequest.clientId)
|
||||
expect(responseBody.clientSecret).toBeUndefined()
|
||||
})
|
||||
|
||||
|
||||
it('Fails if user is not platform owner', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockUser.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/oauth-apps',
|
||||
body: upsertRequest,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Delete OAuth App', () => {
|
||||
it('Forbid by Non Owner', async () => {
|
||||
// arrange
|
||||
const { mockOwner: mockUserTwo, mockPlatform: mockPlatformTwo, mockProject: mockProjectTwo } = await mockAndSaveBasicSetup()
|
||||
|
||||
const mockOAuthApp = await createMockOAuthApp({
|
||||
platformId: mockPlatformTwo.id,
|
||||
})
|
||||
|
||||
await databaseConnection().getRepository('user').update(mockUserTwo.id, {
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
})
|
||||
await databaseConnection().getRepository('oauth_app').save(mockOAuthApp)
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockUserTwo.id,
|
||||
projectId: mockProjectTwo.id,
|
||||
platform: { id: mockPlatformTwo.id },
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/oauth-apps/${mockOAuthApp.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
|
||||
it('By Id', async () => {
|
||||
// arrange
|
||||
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
|
||||
const mockOAuthApp = await createMockOAuthApp({
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
await databaseConnection().getRepository('oauth_app').save(mockOAuthApp)
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockOwner.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/oauth-apps/${mockOAuthApp.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
})
|
||||
})
|
||||
|
||||
describe('List OAuth Apps endpoint', () => {
|
||||
it('should list OAuth Apps by platform owner', async () => {
|
||||
// arrange
|
||||
const { mockOwner: mockUserOne, mockPlatform: mockPlatformOne, mockProject: mockProjectOne } = await mockAndSaveBasicSetup()
|
||||
|
||||
const mockOAuthAppsOne = await createMockOAuthApp({
|
||||
platformId: mockPlatformOne.id,
|
||||
})
|
||||
|
||||
await databaseConnection()
|
||||
.getRepository('oauth_app')
|
||||
.save([mockOAuthAppsOne])
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockUserOne.id,
|
||||
projectId: mockProjectOne.id,
|
||||
platform: { id: mockPlatformOne.id },
|
||||
})
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/oauth-apps',
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(responseBody.data).toHaveLength(1)
|
||||
expect(responseBody.data[0].id).toBe(mockOAuthAppsOne.id)
|
||||
expect(responseBody.data[0].clientSecret).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should list OAuth Apps by platform member', async () => {
|
||||
// arrange
|
||||
const { mockPlatform: mockPlatformOne, mockProject: mockProjectOne } = await mockAndSaveBasicSetup()
|
||||
const { mockOwner: mockUserTwo, mockPlatform: mockPlatformTwo } = await mockAndSaveBasicSetup()
|
||||
|
||||
const mockOAuthAppsOne = await createMockOAuthApp({
|
||||
platformId: mockPlatformOne.id,
|
||||
})
|
||||
const mockOAuthAppsTwo = await createMockOAuthApp({
|
||||
platformId: mockPlatformTwo.id,
|
||||
})
|
||||
|
||||
await databaseConnection()
|
||||
.getRepository('oauth_app')
|
||||
.save([mockOAuthAppsOne, mockOAuthAppsTwo])
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockUserTwo.id,
|
||||
projectId: mockProjectOne.id,
|
||||
platform: { id: mockPlatformOne.id },
|
||||
})
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/oauth-apps',
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(responseBody.data).toHaveLength(1)
|
||||
expect(responseBody.data[0].id).toBe(mockOAuthAppsOne.id)
|
||||
expect(responseBody.data[0].clientSecret).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,122 @@
|
||||
import { OtpType } from '@activepieces/ee-shared'
|
||||
import { FastifyBaseLogger, FastifyInstance } from 'fastify'
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { initializeDatabase } from '../../../../src/app/database'
|
||||
import { databaseConnection } from '../../../../src/app/database/database-connection'
|
||||
import * as emailServiceFile from '../../../../src/app/ee/helper/email/email-service'
|
||||
import { setupServer } from '../../../../src/app/server'
|
||||
import { mockAndSaveBasicSetup } from '../../../helpers/mocks'
|
||||
|
||||
let app: FastifyInstance | null = null
|
||||
let sendOtpSpy: jest.Mock
|
||||
|
||||
beforeAll(async () => {
|
||||
await initializeDatabase({ runMigrations: false })
|
||||
app = await setupServer()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
sendOtpSpy = jest.fn()
|
||||
jest.spyOn(emailServiceFile, 'emailService').mockImplementation((_log: FastifyBaseLogger) => ({
|
||||
sendOtp: sendOtpSpy,
|
||||
sendInvitation: jest.fn(),
|
||||
sendIssueCreatedNotification: jest.fn(),
|
||||
sendQuotaAlert: jest.fn(),
|
||||
sendReminderJobHandler: jest.fn(),
|
||||
sendExceedFailureThresholdAlert: jest.fn(),
|
||||
}))
|
||||
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await databaseConnection().destroy()
|
||||
await app?.close()
|
||||
})
|
||||
|
||||
describe('OTP API', () => {
|
||||
describe('Create and Send Endpoint', () => {
|
||||
it('Generates new OTP', async () => {
|
||||
const { mockUserIdentity } = await mockAndSaveBasicSetup()
|
||||
|
||||
const mockCreateOtpRequest = {
|
||||
email: mockUserIdentity.email,
|
||||
type: OtpType.EMAIL_VERIFICATION,
|
||||
}
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/otp',
|
||||
body: mockCreateOtpRequest,
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.NO_CONTENT)
|
||||
})
|
||||
|
||||
it('Sends OTP to user', async () => {
|
||||
const { mockUserIdentity } = await mockAndSaveBasicSetup()
|
||||
|
||||
await databaseConnection().getRepository('user_identity').update(mockUserIdentity.id, {
|
||||
verified: false,
|
||||
})
|
||||
|
||||
const mockCreateOtpRequest = {
|
||||
email: mockUserIdentity.email,
|
||||
type: OtpType.EMAIL_VERIFICATION,
|
||||
}
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/otp',
|
||||
body: mockCreateOtpRequest,
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.NO_CONTENT)
|
||||
expect(sendOtpSpy).toHaveBeenCalledTimes(1)
|
||||
expect(sendOtpSpy).toHaveBeenCalledWith({
|
||||
otp: expect.stringMatching(/^([0-9A-F]|-){36}$/i),
|
||||
platformId: null,
|
||||
type: OtpType.EMAIL_VERIFICATION,
|
||||
userIdentity: expect.objectContaining({
|
||||
email: mockUserIdentity.email,
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
it('OTP is unique per user per OTP type', async () => {
|
||||
const { mockUserIdentity } = await mockAndSaveBasicSetup()
|
||||
|
||||
const mockCreateOtpRequest = {
|
||||
email: mockUserIdentity.email,
|
||||
type: OtpType.EMAIL_VERIFICATION,
|
||||
}
|
||||
|
||||
// act
|
||||
const response1 = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/otp',
|
||||
body: mockCreateOtpRequest,
|
||||
})
|
||||
|
||||
const response2 = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/otp',
|
||||
body: mockCreateOtpRequest,
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response1?.statusCode).toBe(StatusCodes.NO_CONTENT)
|
||||
expect(response2?.statusCode).toBe(StatusCodes.NO_CONTENT)
|
||||
|
||||
const otpCount = await databaseConnection().getRepository('otp').countBy({
|
||||
identityId: mockUserIdentity.id,
|
||||
type: mockCreateOtpRequest.type,
|
||||
})
|
||||
|
||||
expect(otpCount).toBe(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,653 @@
|
||||
import {
|
||||
apId,
|
||||
FilteredPieceBehavior,
|
||||
PiecesFilterType,
|
||||
PieceType,
|
||||
PlatformRole,
|
||||
PrincipalType,
|
||||
} from '@activepieces/shared'
|
||||
import { FastifyBaseLogger, FastifyInstance } from 'fastify'
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { initializeDatabase } from '../../../../src/app/database'
|
||||
import { databaseConnection } from '../../../../src/app/database/database-connection'
|
||||
import { localPieceCache } from '../../../../src/app/pieces/metadata/local-piece-cache'
|
||||
import { setupServer } from '../../../../src/app/server'
|
||||
import { generateMockToken } from '../../../helpers/auth'
|
||||
import {
|
||||
createMockPieceMetadata,
|
||||
createMockPlan,
|
||||
createMockProject,
|
||||
mockAndSaveBasicSetup,
|
||||
mockBasicUser,
|
||||
} from '../../../helpers/mocks'
|
||||
|
||||
let app: FastifyInstance | null = null
|
||||
let mockLog: FastifyBaseLogger
|
||||
|
||||
beforeAll(async () => {
|
||||
await initializeDatabase({ runMigrations: false })
|
||||
app = await setupServer()
|
||||
mockLog = app!.log!
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await databaseConnection().getRepository('piece_metadata').createQueryBuilder().delete().execute()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await databaseConnection().destroy()
|
||||
await app?.close()
|
||||
})
|
||||
|
||||
describe('Piece Metadata API', () => {
|
||||
describe('List Piece Versions endpoint', () => {
|
||||
it('Should return versions in sorted order for a piece', async () => {
|
||||
// arrange
|
||||
const mockPieceMetadata1 = createMockPieceMetadata({
|
||||
name: '@ap/a',
|
||||
version: '0.0.1',
|
||||
pieceType: PieceType.OFFICIAL,
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('piece_metadata')
|
||||
.save(mockPieceMetadata1)
|
||||
|
||||
const mockPieceMetadata2 = createMockPieceMetadata({
|
||||
name: '@ap/a',
|
||||
version: '0.0.2',
|
||||
pieceType: PieceType.OFFICIAL,
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('piece_metadata')
|
||||
.save(mockPieceMetadata2)
|
||||
|
||||
await localPieceCache(mockLog).setup()
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.UNKNOWN,
|
||||
id: apId(),
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/pieces/versions?release=1.1.1&name=@ap/a',
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
const keys = Object.keys(responseBody)
|
||||
expect(keys).toHaveLength(2)
|
||||
expect(keys[0]).toBe('0.0.1')
|
||||
expect(keys[1]).toBe('0.0.2')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Get Piece metadata', () => {
|
||||
it('Should return metadata when authenticated', async () => {
|
||||
// arrange
|
||||
const mockPieceMetadata = createMockPieceMetadata({
|
||||
name: '@activepieces/a',
|
||||
pieceType: PieceType.OFFICIAL,
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('piece_metadata')
|
||||
.save(mockPieceMetadata)
|
||||
|
||||
await localPieceCache(mockLog).setup()
|
||||
|
||||
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup({
|
||||
platform: {
|
||||
filteredPieceBehavior: FilteredPieceBehavior.BLOCKED,
|
||||
filteredPieceNames: [],
|
||||
},
|
||||
})
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockOwner.id,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/pieces/@activepieces/a',
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(responseBody.id).toBe(mockPieceMetadata.id)
|
||||
})
|
||||
|
||||
it('Should return metadata when not authenticated', async () => {
|
||||
// arrange
|
||||
const mockPieceMetadata = createMockPieceMetadata({
|
||||
name: '@activepieces/a',
|
||||
pieceType: PieceType.OFFICIAL,
|
||||
displayName: 'a',
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('piece_metadata')
|
||||
.save(mockPieceMetadata)
|
||||
|
||||
await localPieceCache(mockLog).setup()
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.UNKNOWN,
|
||||
id: apId(),
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/pieces/@activepieces/a',
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
// Expectations for each attribute
|
||||
expect(responseBody.actions).toEqual(mockPieceMetadata.actions)
|
||||
expect(responseBody.triggers).toEqual(mockPieceMetadata.triggers)
|
||||
expect(responseBody.archiveId).toBe(mockPieceMetadata.archiveId)
|
||||
expect(responseBody.auth).toEqual(mockPieceMetadata.auth)
|
||||
expect(responseBody.description).toBe(mockPieceMetadata.description)
|
||||
expect(responseBody.directoryPath).toBe(mockPieceMetadata.directoryPath)
|
||||
expect(responseBody.displayName).toBe(mockPieceMetadata.displayName)
|
||||
expect(responseBody.id).toBe(mockPieceMetadata.id)
|
||||
expect(responseBody.logoUrl).toBe(mockPieceMetadata.logoUrl)
|
||||
expect(responseBody.maximumSupportedRelease).toBe(
|
||||
mockPieceMetadata.maximumSupportedRelease,
|
||||
)
|
||||
expect(responseBody.minimumSupportedRelease).toBe(
|
||||
mockPieceMetadata.minimumSupportedRelease,
|
||||
)
|
||||
expect(responseBody.packageType).toBe(mockPieceMetadata.packageType)
|
||||
expect(responseBody.pieceType).toBe(mockPieceMetadata.pieceType)
|
||||
expect(responseBody.platformId).toBe(mockPieceMetadata.platformId)
|
||||
expect(responseBody.projectId).toBe(mockPieceMetadata.projectId)
|
||||
expect(responseBody.version).toBe(mockPieceMetadata.version)
|
||||
})
|
||||
})
|
||||
describe('List Piece Metadata endpoint', () => {
|
||||
it('Should list platform pieces', async () => {
|
||||
const { mockOwner, mockPlatform } = await mockAndSaveBasicSetup({
|
||||
platform: {
|
||||
filteredPieceBehavior: FilteredPieceBehavior.BLOCKED,
|
||||
filteredPieceNames: [],
|
||||
},
|
||||
})
|
||||
|
||||
const { mockPlatform: mockPlatform2 } = await mockAndSaveBasicSetup({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
const mockProject = await createProjectAndPlan({
|
||||
platformId: mockPlatform.id,
|
||||
ownerId: mockOwner.id,
|
||||
})
|
||||
|
||||
|
||||
|
||||
// arrange
|
||||
const mockPieceMetadataA = createMockPieceMetadata({
|
||||
name: 'a',
|
||||
pieceType: PieceType.CUSTOM,
|
||||
platformId: mockPlatform.id,
|
||||
displayName: 'a',
|
||||
})
|
||||
const mockPieceMetadataB = createMockPieceMetadata({
|
||||
name: 'b',
|
||||
pieceType: PieceType.OFFICIAL,
|
||||
displayName: 'b',
|
||||
})
|
||||
const mockPieceMetadataC = createMockPieceMetadata({
|
||||
name: 'c',
|
||||
pieceType: PieceType.CUSTOM,
|
||||
platformId: mockPlatform2.id,
|
||||
displayName: 'c',
|
||||
})
|
||||
const mockPieceMetadataD = createMockPieceMetadata({
|
||||
name: 'd',
|
||||
pieceType: PieceType.CUSTOM,
|
||||
platformId: mockPlatform.id,
|
||||
displayName: 'd',
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('piece_metadata')
|
||||
.save([
|
||||
mockPieceMetadataA,
|
||||
mockPieceMetadataB,
|
||||
mockPieceMetadataC,
|
||||
mockPieceMetadataD,
|
||||
])
|
||||
|
||||
await localPieceCache(mockLog).setup()
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
id: mockOwner.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/pieces?release=1.1.1',
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(responseBody).toHaveLength(3)
|
||||
expect(responseBody?.[0].id).toBe(mockPieceMetadataA.id)
|
||||
expect(responseBody?.[1].id).toBe(mockPieceMetadataB.id)
|
||||
expect(responseBody?.[2].id).toBe(mockPieceMetadataD.id)
|
||||
})
|
||||
|
||||
it('Should list correct version by piece name', async () => {
|
||||
// arrange
|
||||
const mockPieceMetadataA = createMockPieceMetadata({
|
||||
name: 'a',
|
||||
pieceType: PieceType.OFFICIAL,
|
||||
displayName: 'a',
|
||||
version: '0.0.1',
|
||||
})
|
||||
const mockPieceMetadataB = createMockPieceMetadata({
|
||||
name: 'a',
|
||||
pieceType: PieceType.OFFICIAL,
|
||||
displayName: 'a',
|
||||
version: '0.0.2',
|
||||
})
|
||||
const mockPieceMetadataC = createMockPieceMetadata({
|
||||
name: 'a',
|
||||
pieceType: PieceType.OFFICIAL,
|
||||
displayName: 'a',
|
||||
version: '0.1.0',
|
||||
})
|
||||
const mockPieceMetadataD = createMockPieceMetadata({
|
||||
name: 'a',
|
||||
pieceType: PieceType.OFFICIAL,
|
||||
displayName: 'a',
|
||||
version: '0.1.1',
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('piece_metadata')
|
||||
.save([mockPieceMetadataA, mockPieceMetadataB, mockPieceMetadataC, mockPieceMetadataD])
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.UNKNOWN,
|
||||
id: apId(),
|
||||
})
|
||||
await localPieceCache(mockLog).setup()
|
||||
|
||||
// act
|
||||
const exactVersionResponse = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/pieces/a?version=0.0.1',
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
const exactVersionResponseBody = exactVersionResponse?.json()
|
||||
expect(exactVersionResponse?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(exactVersionResponseBody?.id).toBe(mockPieceMetadataA.id)
|
||||
|
||||
const telda2VersionResponse = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/pieces/a?version=~0.0.2',
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
const teldaVersion2ResponseBody = telda2VersionResponse?.json()
|
||||
expect(telda2VersionResponse?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(teldaVersion2ResponseBody?.id).toBe(mockPieceMetadataB.id)
|
||||
|
||||
const teldaVersionResponse = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/pieces/a?version=~0.0.1',
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
const teldaVersionResponseBody = teldaVersionResponse?.json()
|
||||
expect(teldaVersionResponse?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(teldaVersionResponseBody?.id).toBe(mockPieceMetadataB.id)
|
||||
|
||||
const notFoundVersionResponse = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/pieces/a?version=~0.1.2',
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
expect(notFoundVersionResponse?.statusCode).toBe(StatusCodes.NOT_FOUND)
|
||||
})
|
||||
|
||||
it('Should list latest version by piece name', async () => {
|
||||
// arrange
|
||||
const mockPieceMetadataA = createMockPieceMetadata({
|
||||
name: 'a',
|
||||
pieceType: PieceType.OFFICIAL,
|
||||
displayName: 'a',
|
||||
version: '0.31.0',
|
||||
})
|
||||
const mockPieceMetadataB = createMockPieceMetadata({
|
||||
name: 'a',
|
||||
pieceType: PieceType.OFFICIAL,
|
||||
displayName: 'a',
|
||||
version: '1.0.0',
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('piece_metadata')
|
||||
.save([mockPieceMetadataA, mockPieceMetadataB])
|
||||
|
||||
await localPieceCache(mockLog).setup()
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.UNKNOWN,
|
||||
id: apId(),
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/pieces?release=1.1.1',
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(responseBody).toHaveLength(1)
|
||||
expect(responseBody?.[0].id).toBe(mockPieceMetadataB.id)
|
||||
})
|
||||
|
||||
|
||||
it('Sorts by piece name', async () => {
|
||||
// arrange
|
||||
const mockPieceMetadataA = createMockPieceMetadata({
|
||||
name: 'a',
|
||||
pieceType: PieceType.OFFICIAL,
|
||||
displayName: 'a',
|
||||
})
|
||||
const mockPieceMetadataB = createMockPieceMetadata({
|
||||
name: 'b',
|
||||
pieceType: PieceType.OFFICIAL,
|
||||
displayName: 'b',
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('piece_metadata')
|
||||
.save([mockPieceMetadataA, mockPieceMetadataB])
|
||||
|
||||
await localPieceCache(mockLog).setup()
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.UNKNOWN,
|
||||
id: apId(),
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/pieces?release=1.1.1',
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(responseBody).toHaveLength(2)
|
||||
expect(responseBody?.[0].id).toBe(mockPieceMetadataA.id)
|
||||
expect(responseBody?.[1].id).toBe(mockPieceMetadataB.id)
|
||||
})
|
||||
|
||||
it('Allows filtered pieces if project filter is set to "ALLOWED"', async () => {
|
||||
// arrange
|
||||
const { mockPlatform } = await mockAndSaveBasicSetup({
|
||||
platform: {
|
||||
filteredPieceBehavior: FilteredPieceBehavior.BLOCKED,
|
||||
filteredPieceNames: [],
|
||||
},
|
||||
})
|
||||
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
|
||||
const mockProject = await createProjectAndPlan({
|
||||
ownerId: mockUser.id,
|
||||
platformId: mockPlatform.id,
|
||||
piecesFilterType: PiecesFilterType.ALLOWED,
|
||||
pieces: ['a'],
|
||||
})
|
||||
|
||||
const mockPieceMetadataA = createMockPieceMetadata({
|
||||
name: 'a',
|
||||
pieceType: PieceType.OFFICIAL,
|
||||
displayName: 'a',
|
||||
})
|
||||
const mockPieceMetadataB = createMockPieceMetadata({
|
||||
name: 'b',
|
||||
pieceType: PieceType.OFFICIAL,
|
||||
displayName: 'b',
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('piece_metadata')
|
||||
.save([mockPieceMetadataA, mockPieceMetadataB])
|
||||
|
||||
await localPieceCache(mockLog).setup()
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
id: mockUser.id,
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/pieces?release=1.1.1',
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(responseBody).toHaveLength(1)
|
||||
expect(responseBody?.[0].id).toBe(mockPieceMetadataA.id)
|
||||
})
|
||||
|
||||
it('Allows filtered pieces if platform filter is set to "ALLOWED"', async () => {
|
||||
// arrange
|
||||
const { mockPlatform } = await mockAndSaveBasicSetup({
|
||||
platform: {
|
||||
filteredPieceNames: ['a'],
|
||||
filteredPieceBehavior: FilteredPieceBehavior.ALLOWED,
|
||||
},
|
||||
})
|
||||
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
|
||||
const mockProject = await createProjectAndPlan({
|
||||
ownerId: mockUser.id,
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
|
||||
const mockPieceMetadataA = createMockPieceMetadata({
|
||||
name: 'a',
|
||||
pieceType: PieceType.OFFICIAL,
|
||||
displayName: 'a',
|
||||
})
|
||||
const mockPieceMetadataB = createMockPieceMetadata({
|
||||
name: 'b',
|
||||
pieceType: PieceType.OFFICIAL,
|
||||
displayName: 'b',
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('piece_metadata')
|
||||
.save([mockPieceMetadataA, mockPieceMetadataB])
|
||||
|
||||
await localPieceCache(mockLog).setup()
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
id: mockUser.id,
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/pieces?release=1.1.1',
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(responseBody).toHaveLength(1)
|
||||
expect(responseBody?.[0].id).toBe(mockPieceMetadataA.id)
|
||||
})
|
||||
|
||||
it('Blocks filtered pieces if platform filter is set to "BLOCKED"', async () => {
|
||||
// arrange
|
||||
const { mockPlatform } = await mockAndSaveBasicSetup({
|
||||
platform: {
|
||||
filteredPieceNames: ['a'],
|
||||
filteredPieceBehavior: FilteredPieceBehavior.BLOCKED,
|
||||
},
|
||||
})
|
||||
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
|
||||
const mockProject = await createProjectAndPlan({
|
||||
ownerId: mockUser.id,
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
|
||||
const mockPieceMetadataA = createMockPieceMetadata({
|
||||
name: 'a',
|
||||
pieceType: PieceType.OFFICIAL,
|
||||
displayName: 'a',
|
||||
})
|
||||
const mockPieceMetadataB = createMockPieceMetadata({
|
||||
name: 'b',
|
||||
pieceType: PieceType.OFFICIAL,
|
||||
displayName: 'b',
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('piece_metadata')
|
||||
.save([mockPieceMetadataA, mockPieceMetadataB])
|
||||
|
||||
await localPieceCache(mockLog).setup()
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
id: mockUser.id,
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/pieces?release=1.1.1',
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(responseBody).toHaveLength(1)
|
||||
expect(responseBody?.[0].id).toBe(mockPieceMetadataB.id)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
async function createProjectAndPlan({
|
||||
platformId,
|
||||
ownerId,
|
||||
piecesFilterType,
|
||||
pieces,
|
||||
}: {
|
||||
platformId: string
|
||||
ownerId: string
|
||||
piecesFilterType?: PiecesFilterType
|
||||
pieces?: string[]
|
||||
}) {
|
||||
const project = createMockProject({
|
||||
platformId,
|
||||
ownerId,
|
||||
})
|
||||
await databaseConnection().getRepository('project').save([project])
|
||||
|
||||
const projectPlan = createMockPlan({
|
||||
projectId: project.id,
|
||||
piecesFilterType,
|
||||
pieces,
|
||||
})
|
||||
await databaseConnection().getRepository('project_plan').save([projectPlan])
|
||||
return project
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
import { apId, FilteredPieceBehavior,
|
||||
PlanName,
|
||||
PlatformRole,
|
||||
PrincipalType,
|
||||
UpdatePlatformRequestBody,
|
||||
} from '@activepieces/shared'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { FastifyInstance } from 'fastify'
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { initializeDatabase } from '../../../../src/app/database'
|
||||
import { databaseConnection } from '../../../../src/app/database/database-connection'
|
||||
import { setupServer } from '../../../../src/app/server'
|
||||
import { generateMockToken } from '../../../helpers/auth'
|
||||
import { checkIfSolutionExistsInDb, createMockSolutionAndSave, createMockUser, mockAndSaveBasicSetup, mockBasicUser } from '../../../helpers/mocks'
|
||||
|
||||
let app: FastifyInstance | null = null
|
||||
|
||||
beforeAll(async () => {
|
||||
|
||||
await initializeDatabase({ runMigrations: false })
|
||||
app = await setupServer()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await databaseConnection().destroy()
|
||||
await app?.close()
|
||||
})
|
||||
|
||||
describe('Platform API', () => {
|
||||
describe('update platform endpoint', () => {
|
||||
it('patches a platform by id', async () => {
|
||||
// arrange
|
||||
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup({
|
||||
plan: {
|
||||
embeddingEnabled: false,
|
||||
},
|
||||
platform: {
|
||||
},
|
||||
})
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockOwner.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
const requestBody: UpdatePlatformRequestBody = {
|
||||
name: 'updated name',
|
||||
primaryColor: 'updated primary color',
|
||||
filteredPieceNames: ['updated filtered piece names'],
|
||||
filteredPieceBehavior: FilteredPieceBehavior.ALLOWED,
|
||||
enforceAllowedAuthDomains: true,
|
||||
allowedAuthDomains: ['yahoo.com'],
|
||||
cloudAuthEnabled: false,
|
||||
emailAuthEnabled: false,
|
||||
}
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/platforms/${mockPlatform.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
body: requestBody,
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(responseBody.id).toBe(mockPlatform.id)
|
||||
expect(responseBody.created).toBeDefined()
|
||||
expect(responseBody.updated).toBeDefined()
|
||||
expect(responseBody.enforceAllowedAuthDomains).toBe(
|
||||
requestBody.enforceAllowedAuthDomains,
|
||||
)
|
||||
expect(responseBody.allowedAuthDomains).toEqual(
|
||||
requestBody.allowedAuthDomains,
|
||||
)
|
||||
expect(responseBody.ownerId).toBe(mockOwner.id)
|
||||
expect(responseBody.emailAuthEnabled).toBe(requestBody.emailAuthEnabled)
|
||||
expect(responseBody.name).toBe('updated name')
|
||||
expect(responseBody.primaryColor).toBe('updated primary color')
|
||||
expect(responseBody.filteredPieceNames).toStrictEqual([
|
||||
'updated filtered piece names',
|
||||
])
|
||||
expect(responseBody.filteredPieceBehavior).toBe('ALLOWED')
|
||||
expect(responseBody.emailAuthEnabled).toBe(false)
|
||||
expect(responseBody.federatedAuthProviders).toStrictEqual({})
|
||||
expect(responseBody.cloudAuthEnabled).toBe(false)
|
||||
}),
|
||||
|
||||
it('updates the platform logo icons', async () => {
|
||||
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup({
|
||||
plan: {
|
||||
embeddingEnabled: false,
|
||||
},
|
||||
platform: {
|
||||
},
|
||||
})
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockOwner.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
const formData = new FormData()
|
||||
formData.append('logoIcon', new Blob([faker.image.urlPlaceholder()], { type: 'image/png' }))
|
||||
formData.append('fullLogo', new Blob([faker.image.urlPlaceholder()], { type: 'image/png' }))
|
||||
formData.append('favIcon', new Blob([faker.image.urlPlaceholder()], { type: 'image/png' }))
|
||||
formData.append('name', 'updated name')
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/platforms/${mockPlatform.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
body: formData,
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(responseBody.id).toBe(mockPlatform.id)
|
||||
expect(responseBody.created).toBeDefined()
|
||||
expect(responseBody.updated).toBeDefined()
|
||||
expect(responseBody.name).toBe('updated name')
|
||||
|
||||
const baseUrl = 'http://localhost:4200/api/v1/platforms/assets'
|
||||
expect(responseBody.logoIconUrl.startsWith(baseUrl)).toBeTruthy()
|
||||
expect(responseBody.fullLogoUrl.startsWith(baseUrl)).toBeTruthy()
|
||||
expect(responseBody.favIconUrl.startsWith(baseUrl)).toBeTruthy()
|
||||
}),
|
||||
|
||||
it('fails if user is not owner', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockUser.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/platforms/${mockPlatform.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
body: {
|
||||
primaryColor: '#000000',
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('get platform endpoint', () => {
|
||||
it('Always Returns non-sensitive information for platform', async () => {
|
||||
// arrange
|
||||
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup({
|
||||
platform: {
|
||||
federatedAuthProviders: {
|
||||
google: {
|
||||
clientId: faker.internet.password(),
|
||||
clientSecret: faker.internet.password(),
|
||||
},
|
||||
saml: {
|
||||
idpCertificate: faker.internet.password(),
|
||||
idpMetadata: faker.internet.password(),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const mockToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockOwner.id,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: `/v1/platforms/${mockPlatform.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
const responseBody = response?.json()
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
|
||||
expect(Object.keys(responseBody).length).toBe(19)
|
||||
expect(responseBody.id).toBe(mockPlatform.id)
|
||||
expect(responseBody.ownerId).toBe(mockOwner.id)
|
||||
expect(responseBody.name).toBe(mockPlatform.name)
|
||||
expect(responseBody.federatedAuthProviders.google).toStrictEqual({
|
||||
clientId: mockPlatform.federatedAuthProviders?.google?.clientId,
|
||||
})
|
||||
expect(responseBody.federatedAuthProviders.saml).toStrictEqual({})
|
||||
expect(responseBody.primaryColor).toBe(mockPlatform.primaryColor)
|
||||
expect(responseBody.logoIconUrl).toBe(mockPlatform.logoIconUrl)
|
||||
expect(responseBody.fullLogoUrl).toBe(mockPlatform.fullLogoUrl)
|
||||
expect(responseBody.favIconUrl).toBe(mockPlatform.favIconUrl)
|
||||
})
|
||||
|
||||
|
||||
it('Fails if user is not a platform member', async () => {
|
||||
const { mockOwner: mockOwner1, mockPlatform: mockPlatform1, mockProject: mockProject1 } = await mockAndSaveBasicSetup()
|
||||
const { mockPlatform: mockPlatform2 } = await mockAndSaveBasicSetup()
|
||||
|
||||
const mockToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockOwner1.id,
|
||||
projectId: mockProject1.id,
|
||||
platform: {
|
||||
id: mockPlatform1.id,
|
||||
},
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: `/v1/platforms/${mockPlatform2.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
}),
|
||||
describe('delete platform endpoint', () => {
|
||||
it('deletes a platform by id', async () => {
|
||||
// arrange
|
||||
const firstAccount = await mockAndSaveBasicSetup( {
|
||||
plan: {
|
||||
plan: PlanName.STANDARD,
|
||||
},
|
||||
})
|
||||
const secondAccount = await mockAndSaveBasicSetup(
|
||||
{
|
||||
plan: {
|
||||
plan: PlanName.STANDARD,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const ownerSolution = await createMockSolutionAndSave({ projectId: firstAccount.mockProject.id, platformId: firstAccount.mockPlatform.id, userId: firstAccount.mockOwner.id })
|
||||
|
||||
const secondSolution = await createMockSolutionAndSave({ projectId: secondAccount.mockProject.id, platformId: secondAccount.mockPlatform.id, userId: secondAccount.mockOwner.id })
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: firstAccount.mockOwner.id,
|
||||
projectId: firstAccount.mockProject.id,
|
||||
platform: { id: firstAccount.mockPlatform.id },
|
||||
})
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/platforms/${firstAccount.mockPlatform.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.NO_CONTENT)
|
||||
const secondSolutionExists = await checkIfSolutionExistsInDb(secondSolution)
|
||||
expect(secondSolutionExists).toBe(true)
|
||||
const ownerSolutionExists = await checkIfSolutionExistsInDb(ownerSolution)
|
||||
expect(ownerSolutionExists).toBe(false)
|
||||
}),
|
||||
it('fails if platform is not eligible for deletion', async () => {
|
||||
// arrange
|
||||
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup( {
|
||||
plan: {
|
||||
plan: PlanName.ENTERPRISE,
|
||||
},
|
||||
})
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockOwner.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/platforms/${mockPlatform.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.UNPROCESSABLE_ENTITY)
|
||||
}),
|
||||
it('fails if user is not owner', async () => {
|
||||
// arrange
|
||||
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup( {
|
||||
plan: {
|
||||
plan: PlanName.STANDARD,
|
||||
},
|
||||
})
|
||||
const secondAccount = await mockAndSaveBasicSetup(
|
||||
{
|
||||
plan: {
|
||||
plan: PlanName.STANDARD,
|
||||
},
|
||||
},
|
||||
)
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockOwner.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/platforms/${secondAccount.mockPlatform.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
}),
|
||||
it('doesn\'t delete user identity if it has other users', async () => {
|
||||
// arrange
|
||||
const firstAccount = await mockAndSaveBasicSetup( {
|
||||
plan: {
|
||||
plan: PlanName.STANDARD,
|
||||
},
|
||||
})
|
||||
const secondPlatform = await mockAndSaveBasicSetup( {
|
||||
plan: {
|
||||
plan: PlanName.STANDARD,
|
||||
},
|
||||
})
|
||||
const secondUser = createMockUser({
|
||||
platformId: secondPlatform.mockPlatform.id,
|
||||
platformRole: PlatformRole.ADMIN,
|
||||
identityId: firstAccount.mockUserIdentity.id,
|
||||
})
|
||||
await databaseConnection().getRepository('user').save(secondUser)
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: firstAccount.mockOwner.id,
|
||||
projectId: firstAccount.mockProject.id,
|
||||
platform: { id: firstAccount.mockPlatform.id },
|
||||
})
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/platforms/${firstAccount.mockPlatform.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.NO_CONTENT)
|
||||
const userIdentityExists = await databaseConnection().getRepository('user_identity').findOneBy({ id: firstAccount.mockUserIdentity.id })
|
||||
expect(userIdentityExists).not.toBeNull()
|
||||
})
|
||||
})
|
||||
describe('get platform endpoint', () => {
|
||||
it('fails if user is not part of the platform', async () => {
|
||||
// arrange
|
||||
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockOwner.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: `/v1/platforms/${apId()}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
}),
|
||||
it('succeeds if user is part of the platform and is not admin', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
await databaseConnection().getRepository('user').save(mockUser)
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockUser.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: `/v1/platforms/${mockPlatform.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,484 @@
|
||||
import {
|
||||
ApiKeyResponseWithValue,
|
||||
UpdateProjectMemberRoleRequestBody,
|
||||
} from '@activepieces/ee-shared'
|
||||
import { DefaultProjectRole, Permission, Platform, PlatformRole, PrincipalType, Project, ProjectRole, RoleType, User } from '@activepieces/shared'
|
||||
import { FastifyInstance } from 'fastify'
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { initializeDatabase } from '../../../../src/app/database'
|
||||
import { databaseConnection } from '../../../../src/app/database/database-connection'
|
||||
import { setupServer } from '../../../../src/app/server'
|
||||
import { generateMockToken } from '../../../helpers/auth'
|
||||
import {
|
||||
createMockProject,
|
||||
createMockProjectMember,
|
||||
createMockProjectRole,
|
||||
mockAndSaveBasicSetup,
|
||||
mockAndSaveBasicSetupWithApiKey,
|
||||
mockBasicUser,
|
||||
} from '../../../helpers/mocks'
|
||||
|
||||
let app: FastifyInstance | null = null
|
||||
|
||||
beforeAll(async () => {
|
||||
await initializeDatabase({ runMigrations: false })
|
||||
app = await setupServer()
|
||||
})
|
||||
|
||||
|
||||
afterAll(async () => {
|
||||
await databaseConnection().destroy()
|
||||
await app?.close()
|
||||
})
|
||||
|
||||
describe('Project Member API', () => {
|
||||
|
||||
|
||||
describe('Update project member role', () => {
|
||||
it('should update a project role for a member', async () => {
|
||||
const { mockOwner: mockUserOne, mockPlatform: mockPlatformOne, mockProject: mockProjectOne } = await mockAndSaveBasicSetup({
|
||||
platform: {
|
||||
},
|
||||
plan: {
|
||||
projectRolesEnabled: true,
|
||||
auditLogEnabled: false,
|
||||
},
|
||||
})
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockUserOne.id,
|
||||
projectId: mockProjectOne.id,
|
||||
platform: { id: mockPlatformOne.id },
|
||||
})
|
||||
|
||||
const projectRole = createMockProjectRole({ platformId: mockPlatformOne.id, type: RoleType.CUSTOM, permissions: [Permission.WRITE_PROJECT_MEMBER] })
|
||||
await databaseConnection().getRepository('project_role').save(projectRole)
|
||||
|
||||
const mockProjectMemberOne = createMockProjectMember({ platformId: mockPlatformOne.id, projectId: mockProjectOne.id, projectRoleId: projectRole.id, userId: mockUserOne.id })
|
||||
await databaseConnection().getRepository('project_member').save(mockProjectMemberOne)
|
||||
|
||||
const request: UpdateProjectMemberRoleRequestBody = {
|
||||
role: 'VIEWER',
|
||||
}
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/project-members/${mockProjectMemberOne.id}`,
|
||||
body: request,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
})
|
||||
|
||||
it('should fail to update project role when user does not have permission', async () => {
|
||||
const { mockPlatform: mockPlatformOne, mockProject: mockProjectOne } = await mockAndSaveBasicSetup({
|
||||
plan: {
|
||||
projectRolesEnabled: true,
|
||||
auditLogEnabled: false,
|
||||
},
|
||||
})
|
||||
|
||||
// Create a user who is not in the project
|
||||
const { mockUser: viewerUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatformOne.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
|
||||
const mockProjectTwo = createMockProject({
|
||||
platformId: mockPlatformOne.id,
|
||||
ownerId: viewerUser.id,
|
||||
})
|
||||
await databaseConnection().getRepository('project').save(mockProjectTwo)
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: viewerUser.id,
|
||||
projectId: mockProjectTwo.id,
|
||||
platform: { id: mockPlatformOne.id },
|
||||
})
|
||||
|
||||
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({
|
||||
name: DefaultProjectRole.VIEWER,
|
||||
}) as ProjectRole
|
||||
|
||||
// Create a project member to try to modify
|
||||
const mockProjectMember = createMockProjectMember({
|
||||
platformId: mockPlatformOne.id,
|
||||
projectId: mockProjectOne.id,
|
||||
projectRoleId: projectRole.id,
|
||||
userId: viewerUser.id,
|
||||
})
|
||||
await databaseConnection().getRepository('project_member').save(mockProjectMember)
|
||||
|
||||
const request: UpdateProjectMemberRoleRequestBody = {
|
||||
role: DefaultProjectRole.ADMIN,
|
||||
}
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/project-members/${mockProjectMember.id}`,
|
||||
body: request,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.NOT_FOUND)
|
||||
})
|
||||
|
||||
it('should fail to update project role when user is admin of another project', async () => {
|
||||
// Create first project with its platform
|
||||
const { mockProject: projectOne, mockPlatform } = await mockAndSaveBasicSetup({
|
||||
plan: {
|
||||
projectRolesEnabled: true,
|
||||
auditLogEnabled: false,
|
||||
},
|
||||
})
|
||||
|
||||
// Create second project admin
|
||||
const { mockUser: adminOfProjectTwo } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
|
||||
const projectTwo = createMockProject({
|
||||
ownerId: adminOfProjectTwo.id,
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
await databaseConnection().getRepository('project').save(projectTwo)
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: adminOfProjectTwo.id,
|
||||
projectId: projectTwo.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
// Create member in first project to try to modify
|
||||
const { mockUser: memberToModify } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
|
||||
const viewerRole = await databaseConnection().getRepository('project_role').findOneByOrFail({
|
||||
name: DefaultProjectRole.VIEWER,
|
||||
}) as ProjectRole
|
||||
|
||||
const projectMember = createMockProjectMember({
|
||||
platformId: mockPlatform.id,
|
||||
projectId: projectOne.id,
|
||||
projectRoleId: viewerRole.id,
|
||||
userId: memberToModify.id,
|
||||
})
|
||||
await databaseConnection().getRepository('project_member').save(projectMember)
|
||||
|
||||
const request: UpdateProjectMemberRoleRequestBody = {
|
||||
role: DefaultProjectRole.ADMIN,
|
||||
}
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/project-members/${projectMember.id}`,
|
||||
body: request,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
expect(response?.statusCode).toBe(StatusCodes.NOT_FOUND)
|
||||
})
|
||||
})
|
||||
|
||||
describe('List project members Endpoint', () => {
|
||||
describe('List project members from api', () => {
|
||||
it('should return project members', async () => {
|
||||
const { mockApiKey, mockProject, mockMember, mockPlatform } = await createBasicEnvironment()
|
||||
|
||||
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.VIEWER }) as ProjectRole
|
||||
|
||||
const mockProjectMember = createMockProjectMember({
|
||||
projectId: mockProject.id,
|
||||
userId: mockMember.id,
|
||||
projectRoleId: projectRole.id,
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('project_member')
|
||||
.save(mockProjectMember)
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: `/v1/project-members?projectId=${mockProject.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockApiKey.value}`,
|
||||
},
|
||||
})
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
const responseBody = response?.json()
|
||||
expect(responseBody.data).toHaveLength(1)
|
||||
expect(responseBody.data[0].id).toBe(mockProjectMember.id)
|
||||
})
|
||||
|
||||
it('Lists project members for non owner project', async () => {
|
||||
const { mockApiKey, mockMember } = await createBasicEnvironment()
|
||||
const { mockProject: mockProject2 } = await mockAndSaveBasicSetup({
|
||||
plan: {
|
||||
projectRolesEnabled: true,
|
||||
auditLogEnabled: false,
|
||||
},
|
||||
})
|
||||
|
||||
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.VIEWER }) as ProjectRole
|
||||
|
||||
const mockProjectMember = createMockProjectMember({
|
||||
projectId: mockProject2.id,
|
||||
userId: mockMember.id,
|
||||
projectRoleId: projectRole.id,
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('project_member')
|
||||
.save(mockProjectMember)
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: `/v1/project-members?projectId=${mockProject2.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockApiKey.value}`,
|
||||
},
|
||||
})
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
})
|
||||
|
||||
describe('List project members by user', () => {
|
||||
|
||||
it.each([
|
||||
DefaultProjectRole.ADMIN,
|
||||
DefaultProjectRole.EDITOR,
|
||||
DefaultProjectRole.VIEWER,
|
||||
DefaultProjectRole.OPERATOR,
|
||||
])('Succeeds if user role is %s', async (testRole) => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject, mockMember } = await createBasicEnvironment()
|
||||
|
||||
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: testRole }) as ProjectRole
|
||||
|
||||
const mockProjectMember = createMockProjectMember({
|
||||
userId: mockMember.id,
|
||||
platformId: mockPlatform.id,
|
||||
projectId: mockProject.id,
|
||||
projectRoleId: projectRole.id,
|
||||
})
|
||||
await databaseConnection().getRepository('project_member').save([mockProjectMember])
|
||||
|
||||
const mockToken = await generateMockToken({
|
||||
id: mockMember.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: `/v1/project-members?projectId=${mockProject.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
describe('Delete project member Endpoint', () => {
|
||||
it('Deletes project member', async () => {
|
||||
const { mockOwnerToken, mockProject, mockMember } = await createBasicEnvironment()
|
||||
|
||||
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.ADMIN }) as ProjectRole
|
||||
|
||||
const mockProjectMember = createMockProjectMember({
|
||||
projectId: mockProject.id,
|
||||
userId: mockMember.id,
|
||||
projectRoleId: projectRole.id,
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('project_member')
|
||||
.save(mockProjectMember)
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/project-members/${mockProjectMember.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockOwnerToken}`,
|
||||
},
|
||||
})
|
||||
expect(response?.statusCode).toBe(StatusCodes.NO_CONTENT)
|
||||
})
|
||||
|
||||
it.each([
|
||||
DefaultProjectRole.EDITOR,
|
||||
DefaultProjectRole.VIEWER,
|
||||
DefaultProjectRole.OPERATOR,
|
||||
])('Fails if user role is %s', async (testRole) => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject, mockMember } = await createBasicEnvironment()
|
||||
|
||||
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: testRole }) as ProjectRole
|
||||
|
||||
const mockProjectMember = createMockProjectMember({
|
||||
userId: mockMember.id,
|
||||
platformId: mockPlatform.id,
|
||||
projectId: mockProject.id,
|
||||
projectRoleId: projectRole.id,
|
||||
})
|
||||
await databaseConnection().getRepository('project_member').save([mockProjectMember])
|
||||
|
||||
const mockToken = await generateMockToken({
|
||||
id: mockMember.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/project-members/${mockProjectMember.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
})
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(responseBody?.code).toBe('PERMISSION_DENIED')
|
||||
expect(responseBody?.params?.userId).toBe(mockMember.id)
|
||||
expect(responseBody?.params?.projectId).toBe(mockProject.id)
|
||||
})
|
||||
|
||||
it('Delete project member from api', async () => {
|
||||
const { mockApiKey, mockProject, mockMember } = await createBasicEnvironment()
|
||||
|
||||
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.ADMIN }) as ProjectRole
|
||||
|
||||
const mockProjectMember = createMockProjectMember({
|
||||
projectId: mockProject.id,
|
||||
userId: mockMember.id,
|
||||
projectRoleId: projectRole.id,
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('project_member')
|
||||
.save(mockProjectMember)
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/project-members/${mockProjectMember.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockApiKey.value}`,
|
||||
},
|
||||
})
|
||||
expect(response?.statusCode).toBe(StatusCodes.NO_CONTENT)
|
||||
})
|
||||
|
||||
it('Delete project member from api for non owner project', async () => {
|
||||
const { mockApiKey, mockMember } = await createBasicEnvironment()
|
||||
const { mockProject: mockProject2 } = await mockAndSaveBasicSetup({
|
||||
plan: {
|
||||
projectRolesEnabled: true,
|
||||
auditLogEnabled: false,
|
||||
},
|
||||
})
|
||||
|
||||
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.ADMIN }) as ProjectRole
|
||||
|
||||
const mockProjectMember = createMockProjectMember({
|
||||
projectId: mockProject2.id,
|
||||
platformId: mockProject2.platformId,
|
||||
userId: mockMember.id,
|
||||
projectRoleId: projectRole.id,
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('project_member')
|
||||
.save(mockProjectMember)
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/project-members/${mockProjectMember.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockApiKey.value}`,
|
||||
},
|
||||
})
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
async function createBasicEnvironment(): Promise<{
|
||||
mockOwner: User
|
||||
mockPlatform: Platform
|
||||
mockProject: Project
|
||||
mockApiKey: ApiKeyResponseWithValue
|
||||
mockOwnerToken: string
|
||||
mockMember: User
|
||||
}> {
|
||||
const { mockOwner, mockPlatform, mockProject, mockApiKey } = await mockAndSaveBasicSetupWithApiKey({
|
||||
plan: {
|
||||
projectRolesEnabled: true,
|
||||
auditLogEnabled: false,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
await databaseConnection().getRepository('user').update(mockOwner.id, {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.ADMIN,
|
||||
})
|
||||
const mockOwnerToken = await generateMockToken({
|
||||
id: mockOwner.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
const { mockUser: mockMember } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
mockOwner,
|
||||
mockPlatform,
|
||||
mockProject,
|
||||
mockApiKey,
|
||||
mockOwnerToken,
|
||||
mockMember,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
CreateProjectReleaseRequestBody,
|
||||
ProjectReleaseType,
|
||||
} from '@activepieces/shared'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { FastifyInstance } from 'fastify'
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { initializeDatabase } from '../../../../src/app/database'
|
||||
import { databaseConnection } from '../../../../src/app/database/database-connection'
|
||||
import { setupServer } from '../../../../src/app/server'
|
||||
import {
|
||||
createMockApiKey,
|
||||
mockAndSaveBasicSetup,
|
||||
} from '../../../helpers/mocks'
|
||||
|
||||
let app: FastifyInstance | null = null
|
||||
|
||||
beforeAll(async () => {
|
||||
await initializeDatabase({ runMigrations: false })
|
||||
app = await setupServer()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await databaseConnection().destroy()
|
||||
await app?.close()
|
||||
})
|
||||
|
||||
|
||||
describe('Create Project Release', () => {
|
||||
it('Fails if projectId does not match', async () => {
|
||||
const { mockPlatform } = await mockAndSaveBasicSetup()
|
||||
const apiKey = createMockApiKey({
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
await databaseConnection().getRepository('api_key').save([apiKey])
|
||||
|
||||
const request: CreateProjectReleaseRequestBody = {
|
||||
name: faker.animal.bird(),
|
||||
description: faker.lorem.sentence(),
|
||||
selectedFlowsIds: [],
|
||||
projectId: faker.string.uuid(),
|
||||
type: ProjectReleaseType.GIT,
|
||||
}
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/project-releases',
|
||||
body: request,
|
||||
headers: {
|
||||
authorization: `Bearer ${apiKey.value}`,
|
||||
},
|
||||
})
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,271 @@
|
||||
import { PlatformRole, PrincipalType, ProjectRole, UpdateProjectRoleRequestBody } from '@activepieces/shared'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { FastifyInstance } from 'fastify'
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { initializeDatabase } from '../../../../src/app/database'
|
||||
import { databaseConnection } from '../../../../src/app/database/database-connection'
|
||||
import { setupServer } from '../../../../src/app/server'
|
||||
import { generateMockToken } from '../../../helpers/auth'
|
||||
import { createMockProjectRole, mockAndSaveBasicSetup, mockBasicUser } from '../../../helpers/mocks'
|
||||
|
||||
let app: FastifyInstance | null = null
|
||||
|
||||
beforeAll(async () => {
|
||||
await initializeDatabase({ runMigrations: false })
|
||||
app = await setupServer()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await databaseConnection().destroy()
|
||||
await app?.close()
|
||||
})
|
||||
|
||||
describe('Project Role API', () => {
|
||||
describe('Create Project Role', () => {
|
||||
it('should create a new project role', async () => {
|
||||
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockOwner.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
const projectRole = createMockProjectRole({ platformId: mockPlatform.id })
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/project-roles',
|
||||
body: projectRole,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.CREATED)
|
||||
const responseBody = response?.json() as ProjectRole
|
||||
expect(responseBody.id).toBeDefined()
|
||||
expect(responseBody.platformId).toBe(mockPlatform.id)
|
||||
expect(responseBody.name).toBe(projectRole.name)
|
||||
expect(responseBody.permissions).toEqual(projectRole.permissions)
|
||||
})
|
||||
|
||||
it('should fail to create a new project role if user is not platform owner', async () => {
|
||||
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockUser.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
const projectRole = createMockProjectRole({ platformId: mockPlatform.id })
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/project-roles',
|
||||
body: projectRole,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('Get Project Role', () => {
|
||||
it('should get all project roles', async () => {
|
||||
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockOwner.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/project-roles',
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
})
|
||||
|
||||
it('should able to get all project roles if user is not platform owner', async () => {
|
||||
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockUser.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/project-roles',
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Update Project Role', () => {
|
||||
it('should update a project role', async () => {
|
||||
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockOwner.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
const projectRole = createMockProjectRole({ platformId: mockPlatform.id })
|
||||
await databaseConnection().getRepository('project_role').save(projectRole)
|
||||
|
||||
const request: UpdateProjectRoleRequestBody = {
|
||||
name: faker.lorem.word(),
|
||||
permissions: ['read', 'write'],
|
||||
}
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/project-roles/${projectRole.id}`,
|
||||
body: request,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
})
|
||||
|
||||
it('should fail to update if user is not platform owner', async () => {
|
||||
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockUser.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
const projectRole = createMockProjectRole({ platformId: mockPlatform.id })
|
||||
await databaseConnection().getRepository('project_role').save(projectRole)
|
||||
|
||||
const request: UpdateProjectRoleRequestBody = {
|
||||
name: faker.lorem.word(),
|
||||
permissions: ['read', 'write'],
|
||||
}
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/project-roles/${projectRole.id}`,
|
||||
body: request,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Delete Project Role', () => {
|
||||
it('should delete a project role', async () => {
|
||||
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockOwner.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
const projectRole = createMockProjectRole({ platformId: mockPlatform.id })
|
||||
await databaseConnection().getRepository('project_role').save(projectRole)
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/project-roles/${projectRole.name}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
})
|
||||
|
||||
it('should fail to delete a project role if user is not platform owner', async () => {
|
||||
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockUser.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
const projectRole = createMockProjectRole({ platformId: mockPlatform.id })
|
||||
await databaseConnection().getRepository('project_role').save(projectRole)
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/project-roles/${projectRole.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
|
||||
it('should fail to delete a project role if project role does not exist', async () => {
|
||||
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockOwner.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/project-roles/${faker.lorem.word()}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.NOT_FOUND)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,66 @@
|
||||
import { ActivepiecesError, ErrorCode, FlowStatus } from '@activepieces/shared'
|
||||
import { FastifyBaseLogger, FastifyInstance } from 'fastify'
|
||||
import { initializeDatabase } from '../../../../src/app/database'
|
||||
import { databaseConnection } from '../../../../src/app/database/database-connection'
|
||||
import { platformProjectService } from '../../../../src/app/ee/projects/platform-project-service'
|
||||
import { setupServer } from '../../../../src/app/server'
|
||||
import { createMockFlow, createMockFlowRun, createMockFlowVersion, mockAndSaveBasicSetup } from '../../../helpers/mocks'
|
||||
|
||||
let app: FastifyInstance | null = null
|
||||
let mockLog: FastifyBaseLogger
|
||||
|
||||
beforeAll(async () => {
|
||||
await initializeDatabase({ runMigrations: false })
|
||||
app = await setupServer()
|
||||
mockLog = app!.log!
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await databaseConnection().destroy()
|
||||
await app?.close()
|
||||
})
|
||||
|
||||
describe('Platform Project Service', () => {
|
||||
describe('Hard delete Project', () => {
|
||||
it('Hard deletes associated flows fails', async () => {
|
||||
// arrange
|
||||
const { mockProject, mockPlatform } = await mockAndSaveBasicSetup()
|
||||
|
||||
const mockFlow = createMockFlow({ projectId: mockProject.id, status: FlowStatus.ENABLED })
|
||||
await databaseConnection().getRepository('flow').save([mockFlow])
|
||||
|
||||
const mockFlowVersion = createMockFlowVersion({ flowId: mockFlow.id })
|
||||
const mockPublishedFlowVersion = createMockFlowVersion({ flowId: mockFlow.id })
|
||||
await databaseConnection().getRepository('flow_version').save([mockFlowVersion, mockPublishedFlowVersion])
|
||||
|
||||
const mockFlowRun = createMockFlowRun({
|
||||
projectId: mockProject.id,
|
||||
flowId: mockFlow.id,
|
||||
flowVersionId: mockPublishedFlowVersion.id,
|
||||
})
|
||||
await databaseConnection().getRepository('flow_run').save([mockFlowRun])
|
||||
|
||||
await databaseConnection().getRepository('flow').update(mockFlow.id, {
|
||||
publishedVersionId: mockPublishedFlowVersion.id,
|
||||
})
|
||||
|
||||
try {
|
||||
// act
|
||||
await platformProjectService(mockLog).hardDelete({ id: mockProject.id, platformId: mockPlatform.id })
|
||||
|
||||
}
|
||||
catch (error) {
|
||||
// assert
|
||||
|
||||
expect(error).toBeInstanceOf(ActivepiecesError)
|
||||
expect((error as ActivepiecesError).error.code).toBe(ErrorCode.VALIDATION)
|
||||
return
|
||||
}
|
||||
throw new Error('Expected error to be thrown because project has enabled flows')
|
||||
|
||||
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,723 @@
|
||||
import {
|
||||
ApiKeyResponseWithValue,
|
||||
UpdateProjectPlatformRequest,
|
||||
} from '@activepieces/ee-shared'
|
||||
import {
|
||||
apId,
|
||||
FlowStatus,
|
||||
Platform,
|
||||
PlatformRole,
|
||||
PrincipalType,
|
||||
Project,
|
||||
User,
|
||||
} from '@activepieces/shared'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { FastifyInstance } from 'fastify'
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { initializeDatabase } from '../../../../src/app/database'
|
||||
import { databaseConnection } from '../../../../src/app/database/database-connection'
|
||||
import { setupServer } from '../../../../src/app/server'
|
||||
import { generateMockToken } from '../../../helpers/auth'
|
||||
import {
|
||||
createMockApiKey,
|
||||
createMockFlow,
|
||||
createMockProject,
|
||||
mockAndSaveBasicSetup,
|
||||
mockBasicUser,
|
||||
} from '../../../helpers/mocks'
|
||||
|
||||
let app: FastifyInstance | null = null
|
||||
|
||||
|
||||
beforeAll(async () => {
|
||||
await initializeDatabase({ runMigrations: false })
|
||||
app = await setupServer()
|
||||
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await databaseConnection().destroy()
|
||||
await app?.close()
|
||||
})
|
||||
|
||||
describe('Project API', () => {
|
||||
describe('Create Project', () => {
|
||||
it('it should create project by user', async () => {
|
||||
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockOwner.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
const displayName = faker.animal.bird()
|
||||
const metadata = { foo: 'bar' }
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/projects',
|
||||
body: {
|
||||
displayName,
|
||||
metadata,
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.CREATED)
|
||||
expect(responseBody.displayName).toBe(displayName)
|
||||
expect(responseBody.ownerId).toBe(mockOwner.id)
|
||||
expect(responseBody.platformId).toBe(mockPlatform.id)
|
||||
expect(responseBody.metadata).toEqual(metadata)
|
||||
})
|
||||
|
||||
it('it should create project by api key', async () => {
|
||||
const { mockOwner: mockUser, mockPlatform } = await mockAndSaveBasicSetup()
|
||||
|
||||
const apiKey = createMockApiKey({
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
await databaseConnection().getRepository('api_key').save([apiKey])
|
||||
|
||||
const displayName = faker.animal.bird()
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/projects',
|
||||
body: {
|
||||
displayName,
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${apiKey.value}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.CREATED)
|
||||
const responseBody = response?.json()
|
||||
expect(responseBody.displayName).toBe(displayName)
|
||||
expect(responseBody.ownerId).toBe(mockUser.id)
|
||||
expect(responseBody.platformId).toBe(mockPlatform.id)
|
||||
})
|
||||
})
|
||||
|
||||
describe('List Projects by api key', () => {
|
||||
it('it should list platform project', async () => {
|
||||
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
await mockAndSaveBasicSetup()
|
||||
|
||||
const apiKey = createMockApiKey({
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
await databaseConnection().getRepository('api_key').save([apiKey])
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/projects',
|
||||
headers: {
|
||||
authorization: `Bearer ${apiKey.value}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(responseBody.data.length).toBe(1)
|
||||
expect(responseBody.data[0].id).toEqual(mockProject.id)
|
||||
})
|
||||
})
|
||||
|
||||
describe('List Projects by user', () => {
|
||||
it('it should list owned projects in platform', async () => {
|
||||
await mockAndSaveBasicSetup()
|
||||
const { mockOwner: mockUserTwo, mockProject: mockProjectTwo, mockPlatform: mockPlatformTwo } = await mockAndSaveBasicSetup()
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockUserTwo.id,
|
||||
projectId: mockProjectTwo.id,
|
||||
platform: {
|
||||
id: mockPlatformTwo.id,
|
||||
},
|
||||
})
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/users/projects',
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(responseBody.data.length).toBe(1)
|
||||
expect(responseBody.data[0].id).toEqual(mockProjectTwo.id)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Update Project', () => {
|
||||
it('it should update project and ignore plan as project owner', async () => {
|
||||
const { mockOwner: mockUser, mockPlatform } = await mockAndSaveBasicSetup()
|
||||
|
||||
mockUser.platformId = mockPlatform.id
|
||||
mockUser.platformRole = PlatformRole.ADMIN
|
||||
|
||||
await databaseConnection().getRepository('user').save(mockUser)
|
||||
|
||||
const mockProject = createMockProject({
|
||||
ownerId: mockUser.id,
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
await databaseConnection().getRepository('project').save([mockProject])
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockUser.id,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
const request: UpdateProjectPlatformRequest = {
|
||||
displayName: faker.animal.bird(),
|
||||
plan: {
|
||||
},
|
||||
}
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/projects/' + mockProject.id,
|
||||
body: request,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
|
||||
expect(responseBody.id).toBe(mockProject.id)
|
||||
expect(responseBody.displayName).toBe(request.displayName)
|
||||
})
|
||||
|
||||
it('it should update project as platform owner with api key', async () => {
|
||||
const { mockProject, mockApiKey } =
|
||||
await createProjectAndPlatformAndApiKey()
|
||||
const request = {
|
||||
displayName: faker.animal.bird(),
|
||||
plan: {
|
||||
},
|
||||
}
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/projects/' + mockProject.id,
|
||||
body: request,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockApiKey.value}`,
|
||||
},
|
||||
})
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
})
|
||||
|
||||
it('it should update project as platform owner', async () => {
|
||||
const { mockProject, mockPlatform, mockUser } =
|
||||
await createProjectAndPlatformAndApiKey()
|
||||
const mockProjectTwo = createMockProject({
|
||||
ownerId: mockUser.id,
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('project')
|
||||
.save([mockProject, mockProjectTwo])
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockUser.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
const request: UpdateProjectPlatformRequest = {
|
||||
displayName: faker.animal.bird(),
|
||||
plan: {
|
||||
},
|
||||
}
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/projects/' + mockProjectTwo.id,
|
||||
body: request,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
const responseBody = response?.json()
|
||||
expect(responseBody.displayName).toBe(request.displayName)
|
||||
})
|
||||
|
||||
it('Fails if user is not platform owner', async () => {
|
||||
const { mockOwner: platformOwnerUser, mockPlatform } = await mockAndSaveBasicSetup()
|
||||
|
||||
const { mockUser: memberUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
|
||||
const mockProject = createMockProject({
|
||||
ownerId: platformOwnerUser.id,
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
const mockProjectTwo = createMockProject({
|
||||
ownerId: platformOwnerUser.id,
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
await databaseConnection()
|
||||
.getRepository('project')
|
||||
.save([mockProject, mockProjectTwo])
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: memberUser.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
const request: UpdateProjectPlatformRequest = {
|
||||
displayName: faker.animal.bird(),
|
||||
}
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/projects/' + mockProjectTwo.id,
|
||||
body: request,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
|
||||
it('Fails if project is deleted', async () => {
|
||||
// arrange
|
||||
const { mockOwner, mockProject } = await mockAndSaveBasicSetup({
|
||||
project: {
|
||||
deleted: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
|
||||
const mockToken = await generateMockToken({
|
||||
id: mockOwner.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockProject.platformId,
|
||||
},
|
||||
})
|
||||
|
||||
const request: UpdateProjectPlatformRequest = {
|
||||
displayName: faker.animal.bird(),
|
||||
}
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/projects/${mockProject.id}`,
|
||||
body: request,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
expect(response?.statusCode).toBe(StatusCodes.NOT_FOUND)
|
||||
expect(responseBody?.code).toBe('ENTITY_NOT_FOUND')
|
||||
expect(responseBody?.params?.entityId).toBe(mockProject.id)
|
||||
expect(responseBody?.params?.entityType).toBe('project')
|
||||
})
|
||||
|
||||
it('it should update project with metadata', async () => {
|
||||
const { mockOwner: mockUser, mockPlatform } = await mockAndSaveBasicSetup()
|
||||
|
||||
mockUser.platformId = mockPlatform.id
|
||||
mockUser.platformRole = PlatformRole.ADMIN
|
||||
|
||||
await databaseConnection().getRepository('user').save(mockUser)
|
||||
|
||||
const mockProject = createMockProject({
|
||||
ownerId: mockUser.id,
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
await databaseConnection().getRepository('project').save([mockProject])
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockUser.id,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
const metadata = { foo: 'bar' }
|
||||
|
||||
const request: UpdateProjectPlatformRequest = {
|
||||
displayName: faker.animal.bird(),
|
||||
metadata,
|
||||
plan: {
|
||||
},
|
||||
}
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/projects/' + mockProject.id,
|
||||
body: request,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(responseBody.id).toBe(mockProject.id)
|
||||
expect(responseBody.displayName).toBe(request.displayName)
|
||||
expect(responseBody.metadata).toEqual(metadata)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Delete Project endpoint', () => {
|
||||
it('Soft deletes project by id', async () => {
|
||||
// arrange
|
||||
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
|
||||
const mockProjectToDelete = createMockProject({ ownerId: mockOwner.id, platformId: mockPlatform.id })
|
||||
await databaseConnection().getRepository('project').save([mockProjectToDelete])
|
||||
|
||||
const mockToken = await generateMockToken({
|
||||
id: mockOwner.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockProject.platformId,
|
||||
},
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/projects/${mockProjectToDelete.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.NO_CONTENT)
|
||||
const deletedProject = await databaseConnection().getRepository('project').findOneBy({ id: mockProjectToDelete.id })
|
||||
expect(deletedProject?.deleted).not.toBeNull()
|
||||
})
|
||||
|
||||
it('Fails if project has enabled flows', async () => {
|
||||
// arrange
|
||||
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
|
||||
const mockProjectToDelete = createMockProject({ ownerId: mockOwner.id, platformId: mockPlatform.id })
|
||||
await databaseConnection().getRepository('project').save([mockProjectToDelete])
|
||||
|
||||
const enabledFlow = createMockFlow({ projectId: mockProjectToDelete.id, status: FlowStatus.ENABLED })
|
||||
await databaseConnection().getRepository('flow').save([enabledFlow])
|
||||
|
||||
const mockToken = await generateMockToken({
|
||||
id: mockOwner.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockProject.platformId,
|
||||
},
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/projects/${mockProjectToDelete.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.CONFLICT)
|
||||
const responseBody = response?.json()
|
||||
expect(responseBody?.code).toBe('VALIDATION')
|
||||
expect(responseBody?.params?.message).toBe('PROJECT_HAS_ENABLED_FLOWS')
|
||||
})
|
||||
|
||||
it('Fails if project to delete is the active project', async () => {
|
||||
// arrange
|
||||
const { mockOwner, mockProject } = await mockAndSaveBasicSetup()
|
||||
|
||||
const mockToken = await generateMockToken({
|
||||
id: mockOwner.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockProject.platformId,
|
||||
},
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/projects/${mockProject.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.CONFLICT)
|
||||
const responseBody = response?.json()
|
||||
expect(responseBody?.code).toBe('VALIDATION')
|
||||
expect(responseBody?.params?.message).toBe('ACTIVE_PROJECT')
|
||||
})
|
||||
|
||||
it('Requires user to be platform owner', async () => {
|
||||
// arrange
|
||||
const { mockOwner, mockProject } = await mockAndSaveBasicSetup()
|
||||
|
||||
await databaseConnection().getRepository('user').update(mockOwner.id, {
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
})
|
||||
const mockToken = await generateMockToken({
|
||||
id: mockOwner.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockProject.platformId,
|
||||
},
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/projects/${mockProject.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
const responseBody = response?.json()
|
||||
expect(responseBody?.code).toBe('AUTHORIZATION')
|
||||
})
|
||||
|
||||
it('Fails if project to delete is not in current platform', async () => {
|
||||
// arrange
|
||||
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
|
||||
const mockProjectToDelete = createMockProject({ ownerId: mockOwner.id, platformId: mockPlatform.id })
|
||||
await databaseConnection().getRepository('project').save([mockProjectToDelete])
|
||||
|
||||
const randomPlatformId = apId()
|
||||
|
||||
const mockToken = await generateMockToken({
|
||||
id: mockOwner.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: randomPlatformId,
|
||||
},
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/projects/${mockProjectToDelete.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('Platform Operator Access', () => {
|
||||
it('Platform operator can access all projects in their platform', async () => {
|
||||
// arrange
|
||||
const { mockOwner, mockPlatform } = await mockAndSaveBasicSetup()
|
||||
|
||||
// Create a platform operator user
|
||||
const { mockUser: operatorUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.OPERATOR,
|
||||
},
|
||||
})
|
||||
|
||||
// Create multiple projects owned by different users
|
||||
const project1 = createMockProject({
|
||||
ownerId: mockOwner.id,
|
||||
platformId: mockPlatform.id,
|
||||
displayName: 'Project 1',
|
||||
})
|
||||
const project2 = createMockProject({
|
||||
ownerId: mockOwner.id,
|
||||
platformId: mockPlatform.id,
|
||||
displayName: 'Project 2',
|
||||
})
|
||||
|
||||
await databaseConnection().getRepository('project').save([project1, project2])
|
||||
|
||||
const operatorToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: operatorUser.id,
|
||||
projectId: project1.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
// act - list projects
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/users/projects',
|
||||
headers: {
|
||||
authorization: `Bearer ${operatorToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
const responseBody = response?.json()
|
||||
// Platform operator should see all projects including the default one
|
||||
expect(responseBody.data.length).toBeGreaterThanOrEqual(2)
|
||||
const projectNames = responseBody.data.map((p: Project) => p.displayName)
|
||||
expect(projectNames).toContain('Project 1')
|
||||
expect(projectNames).toContain('Project 2')
|
||||
})
|
||||
|
||||
it('Platform operator cannot update platform settings', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
|
||||
const { mockUser: operatorUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.OPERATOR,
|
||||
},
|
||||
})
|
||||
|
||||
const operatorToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: operatorUser.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
// act - try to update platform
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/platforms/${mockPlatform.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${operatorToken}`,
|
||||
},
|
||||
body: {
|
||||
name: 'Should not be allowed',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
|
||||
it('Platform member cannot access projects they are not member of', async () => {
|
||||
// arrange
|
||||
const { mockOwner, mockPlatform } = await mockAndSaveBasicSetup()
|
||||
|
||||
// Create a regular platform member
|
||||
const { mockUser: memberUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
|
||||
// Create a project the member is NOT part of
|
||||
const project = createMockProject({
|
||||
ownerId: mockOwner.id,
|
||||
platformId: mockPlatform.id,
|
||||
displayName: 'Restricted Project',
|
||||
})
|
||||
|
||||
await databaseConnection().getRepository('project').save(project)
|
||||
|
||||
const memberToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: memberUser.id,
|
||||
projectId: project.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
// act - list projects
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/users/projects',
|
||||
headers: {
|
||||
authorization: `Bearer ${memberToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
const responseBody = response?.json()
|
||||
expect(responseBody.data).toHaveLength(0) // Should not see any projects
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
|
||||
async function createProjectAndPlatformAndApiKey(): Promise<{
|
||||
mockApiKey: ApiKeyResponseWithValue
|
||||
mockPlatform: Platform
|
||||
mockProject: Project
|
||||
mockUser: User
|
||||
}> {
|
||||
const { mockOwner: mockUser, mockPlatform } = await mockAndSaveBasicSetup()
|
||||
|
||||
mockUser.platformId = mockPlatform.id
|
||||
mockUser.platformRole = PlatformRole.ADMIN
|
||||
await databaseConnection().getRepository('user').save(mockUser)
|
||||
|
||||
const mockProject = createMockProject({
|
||||
ownerId: mockUser.id,
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
await databaseConnection().getRepository('project').save(mockProject)
|
||||
|
||||
const mockApiKey = createMockApiKey({
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
await databaseConnection().getRepository('api_key').save(mockApiKey)
|
||||
|
||||
return {
|
||||
mockApiKey,
|
||||
mockPlatform,
|
||||
mockProject,
|
||||
mockUser,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
import { PlatformRole, PrincipalType } from '@activepieces/shared'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { FastifyInstance } from 'fastify'
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { initializeDatabase } from '../../../../src/app/database'
|
||||
import { databaseConnection } from '../../../../src/app/database/database-connection'
|
||||
import { setupServer } from '../../../../src/app/server'
|
||||
import { generateMockToken } from '../../../helpers/auth'
|
||||
import {
|
||||
createMockSigningKey,
|
||||
mockAndSaveBasicSetup,
|
||||
mockBasicUser,
|
||||
} from '../../../helpers/mocks'
|
||||
|
||||
let app: FastifyInstance | null = null
|
||||
|
||||
beforeAll(async () => {
|
||||
await initializeDatabase({ runMigrations: false })
|
||||
app = await setupServer()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await databaseConnection().destroy()
|
||||
await app?.close()
|
||||
})
|
||||
|
||||
const setupEnabledPlatform = () => mockAndSaveBasicSetup({ plan: { embeddingEnabled: true } })
|
||||
|
||||
describe('Signing Key API', () => {
|
||||
describe('Add Signing Key API', () => {
|
||||
it('Creates new Signing Key', async () => {
|
||||
// arrange
|
||||
const { mockOwner, mockPlatform, mockProject } = await setupEnabledPlatform()
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockOwner.id,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
const mockSigningKeyName = faker.lorem.word()
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/signing-keys',
|
||||
body: {
|
||||
displayName: mockSigningKeyName,
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.CREATED)
|
||||
expect(responseBody.id).toHaveLength(21)
|
||||
expect(responseBody.platformId).toBe(mockPlatform.id)
|
||||
expect(responseBody.publicKey).toBeDefined()
|
||||
expect(responseBody.displayName).toBe(mockSigningKeyName)
|
||||
expect(responseBody.privateKey).toBeDefined()
|
||||
expect(responseBody.algorithm).toBe('RSA')
|
||||
}, 10000)
|
||||
|
||||
it('Fails if user is not platform owner', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject } = await setupEnabledPlatform()
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
|
||||
const mockSigningKey = createMockSigningKey({
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
|
||||
await databaseConnection()
|
||||
.getRepository('signing_key')
|
||||
.save(mockSigningKey)
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockUser.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
const mockSigningKeyName = faker.lorem.word()
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/signing-keys',
|
||||
body: {
|
||||
displayName: mockSigningKeyName,
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Get Signing Key endpoint', () => {
|
||||
it('Finds a Signing Key by id', async () => {
|
||||
// arrange
|
||||
const { mockOwner, mockPlatform, mockProject } = await setupEnabledPlatform()
|
||||
|
||||
const mockSigningKey = createMockSigningKey({
|
||||
platformId: mockPlatform.id,
|
||||
})
|
||||
|
||||
await databaseConnection()
|
||||
.getRepository('signing_key')
|
||||
.save(mockSigningKey)
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockOwner.id,
|
||||
projectId: mockProject.id,
|
||||
platform: { id: mockPlatform.id },
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: `/v1/signing-keys/${mockSigningKey.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(responseBody.id).toBe(mockSigningKey.id)
|
||||
expect(responseBody.platformId).toBe(mockSigningKey.platformId)
|
||||
expect(responseBody.publicKey).toBe(mockSigningKey.publicKey)
|
||||
expect(responseBody.algorithm).toBe(mockSigningKey.algorithm)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Delete Signing Key endpoint', () => {
|
||||
it('Fail if non owner', async () => {
|
||||
// arrange
|
||||
const { mockPlatform: mockPlatformOne, mockProject: mockProjectOne } = await setupEnabledPlatform()
|
||||
|
||||
const { mockUser: nonOwnerUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatformOne.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
|
||||
const mockSigningKey = createMockSigningKey({
|
||||
platformId: mockPlatformOne.id,
|
||||
})
|
||||
|
||||
await databaseConnection()
|
||||
.getRepository('signing_key')
|
||||
.save(mockSigningKey)
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: nonOwnerUser.id,
|
||||
projectId: mockProjectOne.id,
|
||||
platform: {
|
||||
id: mockPlatformOne.id,
|
||||
},
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/signing-keys/${mockSigningKey.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
})
|
||||
|
||||
describe('List Signing Keys endpoint', () => {
|
||||
it('Filters Signing Keys by platform', async () => {
|
||||
const { mockPlatform: mockPlatformTwo, mockProject: mockProjectTwo } = await setupEnabledPlatform()
|
||||
const { mockOwner: mockUserOne, mockPlatform: mockPlatformOne } = await setupEnabledPlatform()
|
||||
|
||||
const mockSigningKeyOne = createMockSigningKey({
|
||||
platformId: mockPlatformOne.id,
|
||||
})
|
||||
|
||||
const mockSigningKeyTwo = createMockSigningKey({
|
||||
platformId: mockPlatformTwo.id,
|
||||
})
|
||||
|
||||
await databaseConnection()
|
||||
.getRepository('signing_key')
|
||||
.save([mockSigningKeyOne, mockSigningKeyTwo])
|
||||
|
||||
const testToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockUserOne.id,
|
||||
projectId: mockProjectTwo.id,
|
||||
platform: {
|
||||
id: mockPlatformOne.id,
|
||||
},
|
||||
})
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/signing-keys',
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(responseBody.data).toHaveLength(1)
|
||||
expect(responseBody.data[0].id).toBe(mockSigningKeyOne.id)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,168 @@
|
||||
import { apId, PlatformRole, PrincipalType, User } from '@activepieces/shared'
|
||||
import { FastifyInstance, LightMyRequestResponse } from 'fastify'
|
||||
import { initializeDatabase } from '../../../../src/app/database'
|
||||
import { databaseConnection } from '../../../../src/app/database/database-connection'
|
||||
import { setupServer } from '../../../../src/app/server'
|
||||
import { generateMockToken } from '../../../helpers/auth'
|
||||
import { mockAndSaveBasicSetup, mockBasicUser } from '../../../helpers/mocks'
|
||||
|
||||
let app: FastifyInstance | null = null
|
||||
|
||||
beforeAll(async () => {
|
||||
await initializeDatabase({ runMigrations: false })
|
||||
app = await setupServer()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await databaseConnection().destroy()
|
||||
await app?.close()
|
||||
})
|
||||
|
||||
describe('Store-entries API', () => {
|
||||
const projectId = apId()
|
||||
let engineToken: string
|
||||
let userToken: string
|
||||
let serviceToken: string
|
||||
let mockUser: User
|
||||
|
||||
beforeEach(async () => {
|
||||
const { mockPlatform } = await mockAndSaveBasicSetup()
|
||||
|
||||
const { mockUser: user } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
mockUser = user
|
||||
|
||||
engineToken = await generateMockToken({
|
||||
type: PrincipalType.ENGINE,
|
||||
id: apId(),
|
||||
projectId,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
userToken = await generateMockToken({
|
||||
type: PrincipalType.USER,
|
||||
id: mockUser.id,
|
||||
projectId,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
serviceToken = await generateMockToken({
|
||||
type: PrincipalType.SERVICE,
|
||||
id: apId(),
|
||||
projectId,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /v1/store-entries', () => {
|
||||
it('should handle token type engineToken correctly and return 200', async () => {
|
||||
const key = 'new_key_1'
|
||||
const response = await makePostRequest(engineToken, key, 'random_value_0')
|
||||
expect(response?.statusCode).toBe(200)
|
||||
})
|
||||
|
||||
it('should handle token type userToken correctly and return 200', async () => {
|
||||
const key = 'new_key_2'
|
||||
const response = await makePostRequest(userToken, key, 'random_value_0')
|
||||
expect(response?.statusCode).toBe(200)
|
||||
})
|
||||
|
||||
it('should handle token type serviceToken correctly and return 401', async () => {
|
||||
const key = 'new_key_3'
|
||||
const response = await makePostRequest(serviceToken, key, 'random_value_0')
|
||||
expect(response?.statusCode).toBe(403)
|
||||
})
|
||||
|
||||
it('should save and update the value', async () => {
|
||||
const key = 'new_key_1'
|
||||
|
||||
let response = await makePostRequest(engineToken, key, 'random_value_0')
|
||||
const firstResponseBody = response?.json()
|
||||
expect(response?.statusCode).toBe(200)
|
||||
expect(firstResponseBody.key).toEqual(key)
|
||||
expect(firstResponseBody.projectId).toEqual(projectId)
|
||||
expect(firstResponseBody.value).toEqual('random_value_0')
|
||||
|
||||
response = await makePostRequest(engineToken, key, 'random_value_1')
|
||||
const secondResponseBody = response?.json()
|
||||
expect(response?.statusCode).toBe(200)
|
||||
expect(secondResponseBody.key).toEqual(key)
|
||||
expect(secondResponseBody.projectId).toEqual(projectId)
|
||||
expect(secondResponseBody.value).toEqual('random_value_1')
|
||||
expect(firstResponseBody.created).toEqual(secondResponseBody.created)
|
||||
})
|
||||
|
||||
it('should return saved value', async () => {
|
||||
const key = 'new_key_2'
|
||||
|
||||
let response = await makePostRequest(engineToken, key, 'random_value_2')
|
||||
expect(response?.statusCode).toBe(200)
|
||||
const saveResponse = response?.json()
|
||||
|
||||
response = await makeGetRequest(engineToken, key)
|
||||
expect(response?.statusCode).toBe(200)
|
||||
const getResponse = response?.json()
|
||||
|
||||
expect(getResponse.key).toEqual(saveResponse.key)
|
||||
expect(getResponse.value).toEqual(saveResponse.value)
|
||||
expect(getResponse.projectId).toEqual(saveResponse.projectId)
|
||||
})
|
||||
|
||||
it('should delete saved value', async () => {
|
||||
const key = 'new_key_3'
|
||||
|
||||
let response = await makePostRequest(engineToken, key, 'random_value_3')
|
||||
expect(response?.statusCode).toBe(200)
|
||||
|
||||
response = await makeDeleteRequest(engineToken, key)
|
||||
expect(response?.statusCode).toBe(200)
|
||||
|
||||
response = await makeGetRequest(engineToken, key)
|
||||
expect(response?.statusCode).toBe(404)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function makePostRequest(testToken: string, key: string, value: string): Promise<LightMyRequestResponse> | undefined {
|
||||
return app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/store-entries/',
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
body: {
|
||||
key,
|
||||
value,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function makeGetRequest(testToken: string, key: string): Promise<LightMyRequestResponse> | undefined {
|
||||
return app?.inject({
|
||||
method: 'GET',
|
||||
url: `/v1/store-entries/?key=${key}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function makeDeleteRequest(testToken: string, key: string): Promise<LightMyRequestResponse> | undefined {
|
||||
return app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/store-entries/?key=${key}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${testToken}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,523 @@
|
||||
import {
|
||||
ApiKeyResponseWithValue,
|
||||
} from '@activepieces/ee-shared'
|
||||
import { DefaultProjectRole, InvitationStatus, InvitationType, Platform, PlatformRole, PrincipalType, Project, ProjectRole, ProjectType, SendUserInvitationRequest, User } from '@activepieces/shared'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { FastifyBaseLogger, FastifyInstance } from 'fastify'
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { initializeDatabase } from '../../../../src/app/database'
|
||||
import { databaseConnection } from '../../../../src/app/database/database-connection'
|
||||
import { emailService } from '../../../../src/app/ee/helper/email/email-service'
|
||||
import { setupServer } from '../../../../src/app/server'
|
||||
import { generateMockToken } from '../../../helpers/auth'
|
||||
import {
|
||||
createMockProjectMember,
|
||||
createMockUserInvitation,
|
||||
mockAndSaveBasicSetupWithApiKey,
|
||||
mockBasicUser,
|
||||
} from '../../../helpers/mocks'
|
||||
|
||||
let app: FastifyInstance | null = null
|
||||
let mockLog: FastifyBaseLogger
|
||||
|
||||
beforeAll(async () => {
|
||||
await initializeDatabase({ runMigrations: false })
|
||||
app = await setupServer()
|
||||
mockLog = app!.log!
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
emailService(mockLog).sendInvitation = jest.fn()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await databaseConnection().destroy()
|
||||
await app?.close()
|
||||
})
|
||||
|
||||
describe('User Invitation API', () => {
|
||||
describe('Invite User', () => {
|
||||
|
||||
it('should return invitation link when smtp is not configured', async () => {
|
||||
const { mockApiKey, mockProject } = await createBasicEnvironment({})
|
||||
|
||||
const mockInviteProjectMemberRequest: SendUserInvitationRequest = {
|
||||
email: faker.internet.email(),
|
||||
type: InvitationType.PLATFORM,
|
||||
platformRole: PlatformRole.ADMIN,
|
||||
}
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/user-invitations',
|
||||
headers: {
|
||||
authorization: `Bearer ${mockApiKey.value}`,
|
||||
},
|
||||
query: {
|
||||
projectId: mockProject.id,
|
||||
},
|
||||
body: mockInviteProjectMemberRequest,
|
||||
})
|
||||
expect(response?.statusCode).toBe(StatusCodes.CREATED)
|
||||
const responseBody = response?.json()
|
||||
expect(responseBody?.link).toBeUndefined()
|
||||
|
||||
const invitationId = responseBody?.id
|
||||
const invitation = await databaseConnection().getRepository('user_invitation').findOneBy({ id: invitationId })
|
||||
expect(invitation?.status).toBe(InvitationStatus.ACCEPTED)
|
||||
})
|
||||
|
||||
it('should have status pending when inviting a user', async () => {
|
||||
const { mockOwnerToken, mockProject } = await createBasicEnvironment({})
|
||||
|
||||
const mockInviteProjectMemberRequest: SendUserInvitationRequest = {
|
||||
email: faker.internet.email(),
|
||||
type: InvitationType.PLATFORM,
|
||||
platformRole: PlatformRole.ADMIN,
|
||||
}
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/user-invitations',
|
||||
headers: {
|
||||
authorization: `Bearer ${mockOwnerToken}`,
|
||||
},
|
||||
query: {
|
||||
projectId: mockProject.id,
|
||||
},
|
||||
body: mockInviteProjectMemberRequest,
|
||||
})
|
||||
expect(response?.statusCode).toBe(StatusCodes.CREATED)
|
||||
const responseBody = response?.json()
|
||||
const invitationId = responseBody?.id
|
||||
const invitation = await databaseConnection().getRepository('user_invitation').findOneBy({ id: invitationId })
|
||||
expect(invitation?.status).toBe(InvitationStatus.PENDING)
|
||||
})
|
||||
|
||||
it('Invite user to Platform Member', async () => {
|
||||
const { mockApiKey, mockProject } = await createBasicEnvironment({})
|
||||
|
||||
const mockInviteProjectMemberRequest: SendUserInvitationRequest = {
|
||||
email: faker.internet.email(),
|
||||
type: InvitationType.PLATFORM,
|
||||
platformRole: PlatformRole.ADMIN,
|
||||
}
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/user-invitations',
|
||||
headers: {
|
||||
authorization: `Bearer ${mockApiKey.value}`,
|
||||
},
|
||||
query: {
|
||||
projectId: mockProject.id,
|
||||
},
|
||||
body: mockInviteProjectMemberRequest,
|
||||
})
|
||||
expect(response?.statusCode).toBe(StatusCodes.CREATED)
|
||||
})
|
||||
|
||||
it('Invite user to other platform project should fail', async () => {
|
||||
const { mockApiKey } = await createBasicEnvironment({})
|
||||
const { mockProject: mockProject2 } = await createBasicEnvironment({})
|
||||
|
||||
const adminRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.ADMIN }) as ProjectRole
|
||||
|
||||
const mockInviteProjectMemberRequest: SendUserInvitationRequest = {
|
||||
projectRole: adminRole.name,
|
||||
email: faker.internet.email(),
|
||||
projectId: mockProject2.id,
|
||||
type: InvitationType.PROJECT,
|
||||
}
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/user-invitations',
|
||||
headers: {
|
||||
authorization: `Bearer ${mockApiKey.value}`,
|
||||
},
|
||||
body: mockInviteProjectMemberRequest,
|
||||
})
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
|
||||
const responseBody = response?.json()
|
||||
expect(responseBody?.code).toBe('AUTHORIZATION')
|
||||
})
|
||||
|
||||
it('should reject invitation to personal project', async () => {
|
||||
const { mockApiKey, mockProject } = await createBasicEnvironment({
|
||||
project: {
|
||||
type: ProjectType.PERSONAL,
|
||||
},
|
||||
})
|
||||
|
||||
const adminRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.ADMIN }) as ProjectRole
|
||||
|
||||
const mockInviteProjectMemberRequest: SendUserInvitationRequest = {
|
||||
projectRole: adminRole.name,
|
||||
email: faker.internet.email(),
|
||||
projectId: mockProject.id,
|
||||
type: InvitationType.PROJECT,
|
||||
}
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/user-invitations',
|
||||
headers: {
|
||||
authorization: `Bearer ${mockApiKey.value}`,
|
||||
},
|
||||
body: mockInviteProjectMemberRequest,
|
||||
})
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.CONFLICT)
|
||||
const responseBody = response?.json()
|
||||
expect(responseBody?.code).toBe('VALIDATION')
|
||||
expect(responseBody?.params?.message).toBe('Project must be a team project')
|
||||
})
|
||||
|
||||
it('Invite user to Project Member using api key', async () => {
|
||||
const { mockApiKey, mockProject } = await createBasicEnvironment({})
|
||||
|
||||
const adminRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.ADMIN }) as ProjectRole
|
||||
|
||||
const mockInviteProjectMemberRequest: SendUserInvitationRequest = {
|
||||
projectRole: adminRole.name,
|
||||
email: faker.internet.email(),
|
||||
projectId: mockProject.id,
|
||||
type: InvitationType.PROJECT,
|
||||
}
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/user-invitations',
|
||||
headers: {
|
||||
authorization: `Bearer ${mockApiKey.value}`,
|
||||
},
|
||||
body: mockInviteProjectMemberRequest,
|
||||
})
|
||||
expect(response?.statusCode).toBe(StatusCodes.CREATED)
|
||||
})
|
||||
|
||||
it('Invite user to Project Member', async () => {
|
||||
const { mockOwnerToken, mockProject } = await createBasicEnvironment({})
|
||||
const adminRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.ADMIN }) as ProjectRole
|
||||
|
||||
const mockInviteProjectMemberRequest: SendUserInvitationRequest = {
|
||||
projectRole: adminRole.name,
|
||||
email: faker.internet.email(),
|
||||
projectId: mockProject.id,
|
||||
type: InvitationType.PROJECT,
|
||||
}
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/user-invitations',
|
||||
headers: {
|
||||
authorization: `Bearer ${mockOwnerToken}`,
|
||||
},
|
||||
body: mockInviteProjectMemberRequest,
|
||||
})
|
||||
expect(response?.statusCode).toBe(StatusCodes.CREATED)
|
||||
})
|
||||
|
||||
it.each([
|
||||
DefaultProjectRole.EDITOR,
|
||||
DefaultProjectRole.VIEWER,
|
||||
DefaultProjectRole.OPERATOR,
|
||||
])('Fails if user role is %s', async (testRole) => {
|
||||
const { mockMember, mockProject } = await createBasicEnvironment({})
|
||||
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: testRole }) as ProjectRole
|
||||
|
||||
const mockInviteProjectMemberRequest: SendUserInvitationRequest = {
|
||||
projectRole: projectRole.name,
|
||||
email: faker.internet.email(),
|
||||
projectId: mockProject.id,
|
||||
type: InvitationType.PROJECT,
|
||||
}
|
||||
const mockProjectMember = createMockProjectMember({
|
||||
userId: mockMember.id,
|
||||
platformId: mockMember.platformId!,
|
||||
projectId: mockProject.id,
|
||||
projectRoleId: projectRole.id,
|
||||
})
|
||||
await databaseConnection().getRepository('project_member').save(mockProjectMember)
|
||||
|
||||
const mockToken = await generateMockToken({
|
||||
id: mockMember.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockProject.platformId,
|
||||
},
|
||||
})
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/user-invitations',
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
body: mockInviteProjectMemberRequest,
|
||||
})
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
|
||||
const responseBody = response?.json()
|
||||
expect(responseBody?.code).toBe('PERMISSION_DENIED')
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
|
||||
describe('List User Invitations', () => {
|
||||
it('should succeed', async () => {
|
||||
const { mockOwnerToken, mockPlatform, mockProject } = await createBasicEnvironment({})
|
||||
|
||||
const adminRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.ADMIN }) as ProjectRole
|
||||
|
||||
const mockUserInvitation = createMockUserInvitation({
|
||||
email: faker.internet.email(),
|
||||
platformId: mockPlatform.id,
|
||||
projectId: mockProject.id,
|
||||
type: InvitationType.PROJECT,
|
||||
projectRole: adminRole,
|
||||
})
|
||||
await databaseConnection().getRepository('user_invitation').save(mockUserInvitation)
|
||||
const listResponse = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/user-invitations',
|
||||
query: {
|
||||
type: InvitationType.PROJECT,
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${mockOwnerToken}`,
|
||||
},
|
||||
})
|
||||
const responseBody = listResponse?.json()
|
||||
expect(listResponse?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(responseBody?.data.length).toBe(1)
|
||||
})
|
||||
|
||||
it('should succeed with API key', async () => {
|
||||
const { mockApiKey, mockPlatform, mockProject } = await createBasicEnvironment({})
|
||||
const adminRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.ADMIN }) as ProjectRole
|
||||
|
||||
const mockUserInvitation = createMockUserInvitation({
|
||||
email: faker.internet.email(),
|
||||
platformId: mockPlatform.id,
|
||||
projectId: mockProject.id,
|
||||
type: InvitationType.PROJECT,
|
||||
status: InvitationStatus.PENDING,
|
||||
projectRole: adminRole,
|
||||
})
|
||||
await databaseConnection().getRepository('user_invitation').save(mockUserInvitation)
|
||||
const listResponse = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/user-invitations',
|
||||
query: {
|
||||
type: InvitationType.PROJECT,
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${mockApiKey.value}`,
|
||||
},
|
||||
})
|
||||
const responseBody = listResponse?.json()
|
||||
expect(listResponse?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(responseBody?.data.length).toBe(1)
|
||||
})
|
||||
|
||||
it('should return empty list with API key from another platform', async () => {
|
||||
const { mockPlatform, mockProject } = await createBasicEnvironment({})
|
||||
|
||||
const adminRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.ADMIN }) as ProjectRole
|
||||
|
||||
const mockUserInvitation = createMockUserInvitation({
|
||||
email: faker.internet.email(),
|
||||
platformId: mockPlatform.id,
|
||||
projectId: mockProject.id,
|
||||
type: InvitationType.PROJECT,
|
||||
projectRole: adminRole,
|
||||
})
|
||||
await databaseConnection().getRepository('user_invitation').save(mockUserInvitation)
|
||||
|
||||
const { mockApiKey: anotherApiKey } = await createBasicEnvironment({})
|
||||
|
||||
const listResponse = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/user-invitations',
|
||||
query: {
|
||||
type: InvitationType.PROJECT,
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${anotherApiKey.value}`,
|
||||
},
|
||||
})
|
||||
const responseBody = listResponse?.json()
|
||||
expect(listResponse?.statusCode).toBe(StatusCodes.OK)
|
||||
expect(responseBody?.data.length).toBe(0)
|
||||
})
|
||||
|
||||
it('should return forbidden when listing invitations for a project owned by another platform using API key', async () => {
|
||||
// Create two separate environments
|
||||
const { mockApiKey: apiKey1 } = await createBasicEnvironment({})
|
||||
const { mockProject: project2 } = await createBasicEnvironment({})
|
||||
|
||||
const adminRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: DefaultProjectRole.ADMIN }) as ProjectRole
|
||||
|
||||
const mockUserInvitation = createMockUserInvitation({
|
||||
email: faker.internet.email(),
|
||||
platformId: project2.platformId,
|
||||
projectId: project2.id,
|
||||
type: InvitationType.PROJECT,
|
||||
status: InvitationStatus.PENDING,
|
||||
projectRole: adminRole,
|
||||
})
|
||||
await databaseConnection().getRepository('user_invitation').save(mockUserInvitation)
|
||||
|
||||
// Attempt to list invitations for project2 using apiKey1
|
||||
const listResponse = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/user-invitations',
|
||||
query: {
|
||||
projectId: project2.id,
|
||||
type: InvitationType.PROJECT,
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${apiKey1.value}`,
|
||||
},
|
||||
})
|
||||
|
||||
expect(listResponse?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
const responseBody = listResponse?.json()
|
||||
expect(responseBody?.code).toBe('AUTHORIZATION')
|
||||
})
|
||||
|
||||
it.each([
|
||||
DefaultProjectRole.EDITOR,
|
||||
DefaultProjectRole.VIEWER,
|
||||
DefaultProjectRole.ADMIN,
|
||||
DefaultProjectRole.OPERATOR,
|
||||
])('Succeed if user role is %s', async (testRole) => {
|
||||
const { mockMember, mockProject } = await createBasicEnvironment({})
|
||||
|
||||
const projectRole = await databaseConnection().getRepository('project_role').findOneByOrFail({ name: testRole }) as ProjectRole
|
||||
|
||||
const mockInviteProjectMemberRequest: SendUserInvitationRequest = {
|
||||
projectRole: projectRole.name,
|
||||
email: faker.internet.email(),
|
||||
projectId: mockProject.id,
|
||||
type: InvitationType.PROJECT,
|
||||
}
|
||||
const mockProjectMember = createMockProjectMember({
|
||||
userId: mockMember.id,
|
||||
platformId: mockMember.platformId!,
|
||||
projectId: mockProject.id,
|
||||
projectRoleId: projectRole.id,
|
||||
})
|
||||
await databaseConnection().getRepository('project_member').save(mockProjectMember)
|
||||
|
||||
const mockToken = await generateMockToken({
|
||||
id: mockMember.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockProject.platformId,
|
||||
},
|
||||
})
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/user-invitations',
|
||||
headers: {
|
||||
authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
query: {
|
||||
type: InvitationType.PROJECT,
|
||||
},
|
||||
body: mockInviteProjectMemberRequest,
|
||||
})
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Delete User Invitation', () => {
|
||||
it('Delete User Invitation', async () => {
|
||||
const { mockOwnerToken, mockPlatform } = await createBasicEnvironment({})
|
||||
const mockUserInvitation = createMockUserInvitation({
|
||||
email: faker.internet.email(),
|
||||
platformId: mockPlatform.id,
|
||||
type: InvitationType.PLATFORM,
|
||||
platformRole: PlatformRole.ADMIN,
|
||||
})
|
||||
await databaseConnection().getRepository('user_invitation').save(mockUserInvitation)
|
||||
const deleteResponse = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/user-invitations/${mockUserInvitation.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockOwnerToken}`,
|
||||
},
|
||||
})
|
||||
expect(deleteResponse?.statusCode).toBe(StatusCodes.NO_CONTENT)
|
||||
})
|
||||
|
||||
it('Delete User Invitation with API key', async () => {
|
||||
const { mockApiKey, mockPlatform } = await createBasicEnvironment({})
|
||||
const mockUserInvitation = createMockUserInvitation({
|
||||
email: faker.internet.email(),
|
||||
platformId: mockPlatform.id,
|
||||
type: InvitationType.PLATFORM,
|
||||
platformRole: PlatformRole.ADMIN,
|
||||
})
|
||||
await databaseConnection().getRepository('user_invitation').save(mockUserInvitation)
|
||||
const deleteResponse = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/user-invitations/${mockUserInvitation.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockApiKey.value}`,
|
||||
},
|
||||
})
|
||||
expect(deleteResponse?.statusCode).toBe(StatusCodes.NO_CONTENT)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
async function createBasicEnvironment({ platform, project }: { platform?: Partial<Platform>, project?: Partial<Project> }): Promise<{
|
||||
mockOwner: User
|
||||
mockPlatform: Platform
|
||||
mockProject: Project
|
||||
mockApiKey: ApiKeyResponseWithValue
|
||||
mockOwnerToken: string
|
||||
mockMember: User
|
||||
}> {
|
||||
const { mockOwner, mockPlatform, mockProject, mockApiKey } = await mockAndSaveBasicSetupWithApiKey({
|
||||
platform: {
|
||||
...platform,
|
||||
},
|
||||
project: {
|
||||
...project,
|
||||
},
|
||||
plan: {
|
||||
projectRolesEnabled: true,
|
||||
auditLogEnabled: false,
|
||||
},
|
||||
})
|
||||
|
||||
const mockOwnerToken = await generateMockToken({
|
||||
id: mockOwner.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
const { mockUser: mockMember } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
mockOwner,
|
||||
mockPlatform,
|
||||
mockProject,
|
||||
mockApiKey,
|
||||
mockOwnerToken,
|
||||
mockMember,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
|
||||
import {
|
||||
PlatformRole,
|
||||
PrincipalType,
|
||||
UserStatus,
|
||||
} from '@activepieces/shared'
|
||||
import { FastifyInstance } from 'fastify'
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { initializeDatabase } from '../../../../src/app/database'
|
||||
import { databaseConnection } from '../../../../src/app/database/database-connection'
|
||||
import { setupServer } from '../../../../src/app/server'
|
||||
import { generateMockToken } from '../../../helpers/auth'
|
||||
import {
|
||||
mockAndSaveBasicSetup,
|
||||
mockAndSaveBasicSetupWithApiKey,
|
||||
mockBasicUser,
|
||||
} from '../../../helpers/mocks'
|
||||
|
||||
let app: FastifyInstance | null = null
|
||||
|
||||
beforeAll(async () => {
|
||||
await initializeDatabase({ runMigrations: false })
|
||||
app = await setupServer()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await databaseConnection().destroy()
|
||||
await app?.close()
|
||||
})
|
||||
|
||||
describe('Enterprise User API', () => {
|
||||
describe('List users endpoint', () => {
|
||||
|
||||
it('Allows service accounts', async () => {
|
||||
// arrange
|
||||
const { mockOwner, mockPlatform, mockApiKey } = await mockAndSaveBasicSetupWithApiKey()
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/users',
|
||||
query: {
|
||||
platformId: mockPlatform.id,
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${mockApiKey.value}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
const responseBody = response?.json()
|
||||
|
||||
expect(Object.keys(responseBody)).toHaveLength(3)
|
||||
expect(responseBody.data).toHaveLength(1)
|
||||
expect(responseBody.data[0].id).toBe(mockOwner.id)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('Update user endpoint', () => {
|
||||
|
||||
it('Failed if own other platform', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
const { mockOwner: mockOwner2, mockPlatform: mockPlatform2 } = await mockAndSaveBasicSetup()
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
status: UserStatus.ACTIVE,
|
||||
},
|
||||
})
|
||||
|
||||
const mockUserToken = await generateMockToken({
|
||||
id: mockOwner2.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform2.id,
|
||||
},
|
||||
})
|
||||
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/users/${mockUser.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockUserToken}`,
|
||||
},
|
||||
body: {
|
||||
status: UserStatus.INACTIVE,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.NOT_FOUND)
|
||||
})
|
||||
|
||||
it('Fail if not admin', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject } = await mockAndSaveBasicSetup()
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
status: UserStatus.ACTIVE,
|
||||
},
|
||||
})
|
||||
|
||||
const mockUserToken = await generateMockToken({
|
||||
id: mockUser.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/users/${mockUser.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockUserToken}`,
|
||||
},
|
||||
body: {
|
||||
status: UserStatus.INACTIVE,
|
||||
},
|
||||
})
|
||||
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
|
||||
it('Allows service accounts to activate', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockApiKey } = await mockAndSaveBasicSetupWithApiKey()
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
status: UserStatus.INACTIVE,
|
||||
},
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/users/${mockUser.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockApiKey.value}`,
|
||||
},
|
||||
body: {
|
||||
status: UserStatus.ACTIVE,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.OK)
|
||||
|
||||
const responseJson = response?.json()
|
||||
expect(responseJson.id).toBe(mockUser.id)
|
||||
expect(responseJson.password).toBeUndefined()
|
||||
expect(responseJson.status).toBe(UserStatus.ACTIVE)
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
|
||||
describe('Delete user endpoint', () => {
|
||||
|
||||
it('Fails if user is not platform owner', async () => {
|
||||
// arrange
|
||||
const { mockPlatform, mockProject } = await mockAndSaveBasicSetupWithApiKey()
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
|
||||
const mockUserToken = await generateMockToken({
|
||||
id: mockUser.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/users/${mockUser.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockUserToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.FORBIDDEN)
|
||||
})
|
||||
|
||||
it('Allows platform owner to delete user', async () => {
|
||||
// arrange
|
||||
const { mockOwner, mockPlatform, mockProject } = await mockAndSaveBasicSetupWithApiKey()
|
||||
const { mockUser } = await mockBasicUser({
|
||||
user: {
|
||||
platformId: mockPlatform.id,
|
||||
platformRole: PlatformRole.MEMBER,
|
||||
},
|
||||
})
|
||||
|
||||
const mockOwnerToken = await generateMockToken({
|
||||
id: mockOwner.id,
|
||||
type: PrincipalType.USER,
|
||||
projectId: mockProject.id,
|
||||
platform: {
|
||||
id: mockPlatform.id,
|
||||
},
|
||||
})
|
||||
|
||||
// act
|
||||
const response = await app?.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/users/${mockUser.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${mockOwnerToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// assert
|
||||
expect(response?.statusCode).toBe(StatusCodes.NO_CONTENT)
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user