Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 21 additions & 42 deletions __tests__/schema/opportunity.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import type { ZodError } from 'zod';
import { DataSource, IsNull } from 'typeorm';
import request from 'supertest';
import { Alerts, Feed, Keyword, User } from '../../src/entity';
import { Alerts, Keyword, User } from '../../src/entity';
import { Opportunity } from '../../src/entity/opportunities/Opportunity';
import { OpportunityMatch } from '../../src/entity/OpportunityMatch';
import { Organization } from '../../src/entity/Organization';
import {
ContentPreferenceOrganization,
ContentPreferenceOrganizationStatus,
} from '../../src/entity/contentPreference/ContentPreferenceOrganization';

import { OpportunityKeyword } from '../../src/entity/OpportunityKeyword';
import { DatasetLocation } from '../../src/entity/dataset/DatasetLocation';
import { OpportunityLocation } from '../../src/entity/opportunities/OpportunityLocation';
Expand Down Expand Up @@ -5737,12 +5734,12 @@ describe('mutation parseOpportunity', () => {
describe('mutation createSharedSlackChannel', () => {
const MUTATION = /* GraphQL */ `
mutation CreateSharedSlackChannel(
$organizationId: ID!
$opportunityId: ID!
$email: String!
$channelName: String!
) {
createSharedSlackChannel(
organizationId: $organizationId
opportunityId: $opportunityId
email: $email
channelName: $channelName
) {
Expand All @@ -5755,22 +5752,6 @@ describe('mutation createSharedSlackChannel', () => {
// Reset all mocks before each test
mockConversationsCreate.mockReset();
mockConversationsInviteShared.mockReset();

// Add organization membership for user 1
await con.getRepository(Feed).save({
id: '1',
name: 'My Feed',
userId: '1',
});
await saveFixtures(con, ContentPreferenceOrganization, [
{
userId: '1',
organizationId: '550e8400-e29b-41d4-a716-446655440000',
referenceId: '550e8400-e29b-41d4-a716-446655440000',
status: ContentPreferenceOrganizationStatus.Free,
feedId: '1',
},
]);
});

it('should require authentication', async () => {
Expand All @@ -5779,7 +5760,7 @@ describe('mutation createSharedSlackChannel', () => {
{
mutation: MUTATION,
variables: {
organizationId: '550e8400-e29b-41d4-a716-446655440000',
opportunityId: '550e8400-e29b-41d4-a716-446655440001',
email: 'user@example.com',
channelName: 'test-channel',
},
Expand All @@ -5796,7 +5777,7 @@ describe('mutation createSharedSlackChannel', () => {
{
mutation: MUTATION,
variables: {
organizationId: '550e8400-e29b-41d4-a716-446655440000',
opportunityId: '550e8400-e29b-41d4-a716-446655440001',
email: 'user@example.com',
channelName: 'test-channel',
},
Expand Down Expand Up @@ -5830,7 +5811,7 @@ describe('mutation createSharedSlackChannel', () => {

const res = await client.mutate(MUTATION, {
variables: {
organizationId: '550e8400-e29b-41d4-a716-446655440000',
opportunityId: '550e8400-e29b-41d4-a716-446655440001',
email: 'user@example.com',
channelName: 'test-channel',
},
Expand Down Expand Up @@ -5878,7 +5859,7 @@ describe('mutation createSharedSlackChannel', () => {

const res = await client.mutate(MUTATION, {
variables: {
organizationId: '550e8400-e29b-41d4-a716-446655440000',
opportunityId: '550e8400-e29b-41d4-a716-446655440001',
email: 'user@example.com',
channelName: 'existing-channel',
},
Expand Down Expand Up @@ -5915,7 +5896,7 @@ describe('mutation createSharedSlackChannel', () => {

const res = await client.mutate(MUTATION, {
variables: {
organizationId: '550e8400-e29b-41d4-a716-446655440000',
opportunityId: '550e8400-e29b-41d4-a716-446655440001',
email: 'user@example.com',
channelName: 'test-channel',
},
Expand All @@ -5925,14 +5906,14 @@ describe('mutation createSharedSlackChannel', () => {
});

it('should forbid non-members from creating slack channels', async () => {
loggedUser = '2'; // User 2 is not a member of organization 550e8400
loggedUser = '2'; // User 2 is not a recrioter of opporunity/org 550e8400

await testMutationErrorCode(
client,
{
mutation: MUTATION,
variables: {
organizationId: '550e8400-e29b-41d4-a716-446655440000',
opportunityId: '550e8400-e29b-41d4-a716-446655440001',
email: 'user@example.com',
channelName: 'test-channel',
},
Expand All @@ -5942,24 +5923,22 @@ describe('mutation createSharedSlackChannel', () => {
});

it('should require active subscription', async () => {
loggedUser = '2';
loggedUser = '1';

// Create organization membership for user 2
await con.getRepository(ContentPreferenceOrganization).save({
userId: '2',
organizationId: 'ed487a47-6f4d-480f-9712-f48ab29db27c',
referenceId: 'ed487a47-6f4d-480f-9712-f48ab29db27c',
status: ContentPreferenceOrganizationStatus.Free,
feedId: '1',
// Create a recruiter record for the logged-in user
await con.getRepository(OpportunityUserRecruiter).save({
opportunityId: '550e8400-e29b-41d4-a716-446655440001',
userId: '1',
type: OpportunityUserType.Recruiter,
});

// Update organization to have inactive subscription
await con.getRepository(Organization).update(
{ id: 'ed487a47-6f4d-480f-9712-f48ab29db27c' },
{ id: '550e8400-e29b-41d4-a716-446655440000' },
{
recruiterSubscriptionFlags: updateRecruiterSubscriptionFlags({
subscriptionId: 'sub_456',
status: SubscriptionStatus.Pending,
status: SubscriptionStatus.Cancelled,
provider: 'paddle',
items: [{ priceId: 'pri_456', quantity: 3 }],
}),
Expand All @@ -5971,7 +5950,7 @@ describe('mutation createSharedSlackChannel', () => {
{
mutation: MUTATION,
variables: {
organizationId: 'ed487a47-6f4d-480f-9712-f48ab29db27c',
opportunityId: '550e8400-e29b-41d4-a716-446655440001',
email: 'user@example.com',
channelName: 'test-channel',
},
Expand Down Expand Up @@ -6009,7 +5988,7 @@ describe('mutation createSharedSlackChannel', () => {
{
mutation: MUTATION,
variables: {
organizationId: '550e8400-e29b-41d4-a716-446655440000',
opportunityId: '550e8400-e29b-41d4-a716-446655440001',
email: 'user@example.com',
channelName: 'new-channel',
},
Expand Down
2 changes: 2 additions & 0 deletions src/common/opportunity/accessControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export enum OpportunityPermissions {
Edit = 'opportunity_edit',
UpdateState = 'opportunity_update_state',
ViewDraft = 'opportunity_view_draft',
CreateSlackChannel = 'opportunity_create_slack_channel',
}

export const ensureOpportunityPermissions = async ({
Expand Down Expand Up @@ -43,6 +44,7 @@ export const ensureOpportunityPermissions = async ({
OpportunityPermissions.Edit,
OpportunityPermissions.UpdateState,
OpportunityPermissions.ViewDraft,
OpportunityPermissions.CreateSlackChannel,
].includes(permission)
) {
const opportunityUserQb = con
Expand Down
2 changes: 1 addition & 1 deletion src/common/schema/opportunities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ export const reimportOpportunitySchema = z
);

export const createSharedSlackChannelSchema = z.object({
organizationId: z.string().uuid('Organization ID must be a valid UUID'),
opportunityId: z.uuid('Opportunity ID must be a valid UUID'),
email: z.string().email('Email must be a valid email address'),
channelName: z
.string()
Expand Down
56 changes: 17 additions & 39 deletions src/schema/opportunity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ import { QuestionScreening } from '../entity/questions/QuestionScreening';
import { In, Not, JsonContains, EntityManager } from 'typeorm';
import { Organization } from '../entity/Organization';
import { Source, SourceType } from '../entity/Source';
import { ContentPreferenceOrganization } from '../entity/contentPreference/ContentPreferenceOrganization';
import {
OrganizationLinkType,
SocialMediaType,
Expand Down Expand Up @@ -904,9 +903,9 @@ export const typeDefs = /* GraphQL */ `
"""
createSharedSlackChannel(
"""
Organization ID
Opportunity ID
"""
organizationId: ID!
opportunityId: ID!

"""
Email address of the user to invite
Expand Down Expand Up @@ -2493,47 +2492,26 @@ export const resolvers: IResolvers<unknown, BaseContext> = traceResolvers<
payload: z.infer<typeof createSharedSlackChannelSchema>,
ctx: AuthContext,
): Promise<GQLEmptyResponse> => {
const { organizationId, channelName, email } =
const { opportunityId, channelName, email } =
createSharedSlackChannelSchema.parse(payload);

// Check if the user is a recruiter
const isRecruiter = await ctx.con
.getRepository(OpportunityUserRecruiter)
.findOne({
where: {
userId: ctx.userId,
type: OpportunityUserType.Recruiter,
},
});

if (!isRecruiter) {
throw new ForbiddenError(
'Access denied! Only recruiters can create Slack channels',
);
}
await ensureOpportunityPermissions({
con: ctx.con.manager,
userId: ctx.userId,
opportunityId,
permission: OpportunityPermissions.CreateSlackChannel,
isTeamMember: ctx.isTeamMember,
});

// Verify user is a member of the organization
const organizationMembership = await ctx.con
.getRepository(ContentPreferenceOrganization)
.findOne({
where: {
userId: ctx.userId,
organizationId,
},
const opportunity = await ctx.con
.getRepository(OpportunityJob)
.findOneOrFail({
where: { id: opportunityId },
relations: { organization: true },
});

if (!organizationMembership) {
throw new ForbiddenError(
'Access denied! You are not a member of this organization',
);
}

// Get the organization and check subscription status
const organization = await ctx.con
.getRepository(Organization)
.findOneOrFail({
where: { id: organizationId },
});
const organization = await opportunity.organization;

// Check if organization has an active subscription
if (
Expand Down Expand Up @@ -2570,7 +2548,7 @@ export const resolvers: IResolvers<unknown, BaseContext> = traceResolvers<

// Mark organization as having a Slack connection and store channel name
await ctx.con.getRepository(Organization).update(
{ id: organizationId },
{ id: organization.id },
{
recruiterSubscriptionFlags:
updateRecruiterSubscriptionFlags<Organization>({
Expand Down
Loading