fix(billing): only allow PRO plan to self-update their plan limits (#7867)

This commit is contained in:
Dotan Simha 2026-03-25 08:56:27 +02:00 committed by GitHub
parent 3330308ef5
commit ebe8df69fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 81 additions and 2 deletions

View file

@ -53,7 +53,12 @@ import {
updateTargetValidationSettings,
} from './flow';
import * as GraphQLSchema from './gql/graphql';
import { ProjectType, SchemaPolicyInput, TargetAccessScope } from './gql/graphql';
import {
ProjectType,
SchemaPolicyInput,
TargetAccessScope,
UpdateOrgRateLimitDocument,
} from './gql/graphql';
import { execute } from './graphql';
import { createOIDCIntegration } from './oidc-integration.js';
import {
@ -197,6 +202,31 @@ export function initSeed() {
return {
organization,
async overrideOrgPlan(plan: 'PRO' | 'ENTERPRISE' | 'HOBBY') {
const pool = await createConnectionPool();
await pool.query(sql`
UPDATE organizations SET plan_name = ${plan} WHERE id = ${organization.id}
`);
await pool.end();
},
async updateOrgRateLimit(newLimit: number, token = ownerToken) {
const result = await execute({
document: UpdateOrgRateLimitDocument,
variables: {
selector: {
organizationSlug: organization.slug,
},
monthlyLimits: {
operations: newLimit,
},
},
authToken: token,
}).then(r => r.expectNoGraphQLErrors());
return result.updateOrgRateLimit;
},
async createOrganizationAccessToken(
args: {
permissions: Array<string>;

View file

@ -0,0 +1,38 @@
import { initSeed } from '../../../testkit/seed';
test.concurrent(
'should not allow HOBBY organization to use updateOrgRateLimit mutation',
async ({ expect }) => {
const { createOrg } = await initSeed().createOwner();
const { updateOrgRateLimit, overrideOrgPlan } = await createOrg();
await expect(updateOrgRateLimit(50000000)).rejects.toThrowError(
'Only PRO organizations can update rate limits via API',
);
},
);
test.concurrent(
'should not allow ENTERPRISE organization to use updateOrgRateLimit mutation',
async ({ expect }) => {
const { createOrg } = await initSeed().createOwner();
const { updateOrgRateLimit, overrideOrgPlan } = await createOrg();
await overrideOrgPlan('ENTERPRISE');
await expect(updateOrgRateLimit(50000000)).rejects.toThrowError(
'Only PRO organizations can update rate limits via API',
);
},
);
test.concurrent(
'should only allow PRO organization to use updateOrgRateLimit mutation',
async ({ expect }) => {
const { createOrg } = await initSeed().createOwner();
const { overrideOrgPlan, updateOrgRateLimit } = await createOrg();
await overrideOrgPlan('PRO');
// Not throwing - should succeed
await updateOrgRateLimit(50000000);
},
);

View file

@ -14,7 +14,8 @@ function filterEmailsByOrg(orgSlug: string, emails: emails.Email[]) {
test('rate limit approaching and reached for organization', async () => {
const { createOrg, ownerToken, ownerEmail } = await initSeed().createOwner();
const { createProject, organization } = await createOrg();
const { createProject, organization, overrideOrgPlan } = await createOrg();
await overrideOrgPlan('PRO');
const { createTargetAccessToken, waitForRequestsCollected } = await createProject(
ProjectType.Single,
);

View file

@ -1,5 +1,7 @@
import { HiveError } from '../../../../shared/errors';
import { OrganizationManager } from '../../../organization/providers/organization-manager';
import { IdTranslator } from '../../../shared/providers/id-translator';
import { Storage } from '../../../shared/providers/storage';
import { USAGE_DEFAULT_LIMITATIONS } from '../../constants';
import type { MutationResolvers } from './../../../../__generated__/types';
@ -12,6 +14,14 @@ export const updateOrgRateLimit: NonNullable<MutationResolvers['updateOrgRateLim
organizationSlug: args.selector.organizationSlug,
});
const organization = await injector.get(Storage).getOrganization({
organizationId: organizationId,
});
if (organization.billingPlan !== 'PRO') {
throw new HiveError('Only PRO organizations can update rate limits via API');
}
return injector.get(OrganizationManager).updateRateLimits({
organizationId: organizationId,
monthlyRateLimit: {