integrationFeatures.spec.tsx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. import {Fragment} from 'react';
  2. import {OrganizationFixture} from 'sentry-fixture/organization';
  3. import {UserFixture} from 'sentry-fixture/user';
  4. import {BillingConfigFixture} from 'getsentry-test/fixtures/billingConfig';
  5. import {PlanDetailsLookupFixture} from 'getsentry-test/fixtures/planDetailsLookup';
  6. import {SubscriptionFixture} from 'getsentry-test/fixtures/subscription';
  7. import {render, screen, waitFor} from 'sentry-test/reactTestingLibrary';
  8. import ConfigStore from 'sentry/stores/configStore';
  9. import hookIntegrationFeatures from 'getsentry/hooks/integrationFeatures';
  10. import SubscriptionStore from 'getsentry/stores/subscriptionStore';
  11. import {PlanTier} from 'getsentry/types';
  12. describe('hookIntegrationFeatures', function () {
  13. const {FeatureList, IntegrationFeatures} = hookIntegrationFeatures();
  14. const organization = OrganizationFixture({experiments: {}});
  15. ConfigStore.set('user', UserFixture({isSuperuser: true}));
  16. beforeEach(() => {
  17. MockApiClient.addMockResponse({
  18. url: `/customers/${organization.slug}/billing-config/`,
  19. query: {tier: 'am2'},
  20. body: BillingConfigFixture(PlanTier.AM2),
  21. });
  22. });
  23. it('does not gate free-only feature sets', async function () {
  24. const sub = SubscriptionFixture({organization});
  25. SubscriptionStore.set(organization.slug, sub);
  26. const features = [
  27. {
  28. description: 'Some non-plan feature',
  29. featureGate: 'non-plan-feature',
  30. },
  31. {
  32. description: 'Another non-plan feature',
  33. featureGate: 'non-plan-feature2',
  34. },
  35. ];
  36. const renderCallback = jest.fn(() => <Fragment />);
  37. render(
  38. <IntegrationFeatures {...{organization, features}}>
  39. {renderCallback}
  40. </IntegrationFeatures>
  41. );
  42. await waitFor(() => {
  43. expect(renderCallback).toHaveBeenCalledWith({
  44. disabled: false,
  45. disabledReason: null,
  46. ungatedFeatures: features,
  47. gatedFeatureGroups: [],
  48. });
  49. });
  50. });
  51. it('gates premium only features and requires upgrade with free plan', async function () {
  52. const sub = SubscriptionFixture({organization});
  53. SubscriptionStore.set(organization.slug, sub);
  54. const features = [
  55. {
  56. description: 'Some non-plan feature',
  57. featureGate: 'integrations-issue-basic',
  58. },
  59. {
  60. description: 'Another non-plan feature',
  61. featureGate: 'integrations-event-hooks',
  62. },
  63. ];
  64. const renderCallback = jest.fn(() => <Fragment />);
  65. render(
  66. <IntegrationFeatures {...{organization, features}}>
  67. {renderCallback}
  68. </IntegrationFeatures>
  69. );
  70. await waitFor(() => {
  71. expect(renderCallback).toHaveBeenCalledWith({
  72. disabled: true,
  73. disabledReason: expect.anything(), // TODO use matching that will work with a React component
  74. ungatedFeatures: [],
  75. gatedFeatureGroups: [
  76. {
  77. plan: PlanDetailsLookupFixture('am2_team'),
  78. features: [features[0]],
  79. hasFeatures: false,
  80. },
  81. {
  82. plan: PlanDetailsLookupFixture('am2_business'),
  83. features: [features[1]],
  84. hasFeatures: false,
  85. },
  86. ],
  87. });
  88. });
  89. });
  90. describe('FeatureList and IntegrationFeatures that distinguish free and premium', function () {
  91. const sub = SubscriptionFixture({organization, plan: 'am2_team'});
  92. SubscriptionStore.set(organization.slug, sub);
  93. // Fixtures do not add features based on plan. Manually add the
  94. // integrations-issue-basic feature for this organizations feature
  95. beforeEach(() => {
  96. organization.features = ['integrations-issue-basic'];
  97. MockApiClient.addMockResponse({
  98. url: `/customers/${organization.slug}/billing-config/`,
  99. query: {tier: 'am2'},
  100. body: BillingConfigFixture(PlanTier.AM2),
  101. });
  102. });
  103. afterEach(() => {
  104. organization.features = [];
  105. });
  106. const features = [
  107. {
  108. description: 'Some non-plan feature',
  109. featureGate: 'non-plan-feature',
  110. },
  111. {
  112. description: 'Issue basic plan feature',
  113. featureGate: 'integrations-issue-basic',
  114. },
  115. {
  116. description: 'Event hooks plan feature',
  117. featureGate: 'integrations-event-hooks',
  118. },
  119. ];
  120. it('renders with the correct callback', async function () {
  121. const renderCallback = jest.fn(() => <Fragment />);
  122. render(
  123. <IntegrationFeatures {...{organization, features}}>
  124. {renderCallback}
  125. </IntegrationFeatures>
  126. );
  127. await waitFor(() => {
  128. expect(renderCallback).toHaveBeenCalledWith({
  129. disabled: false,
  130. disabledReason: null,
  131. ungatedFeatures: [features[0]],
  132. gatedFeatureGroups: [
  133. {
  134. plan: PlanDetailsLookupFixture('am2_team'),
  135. features: [features[1]],
  136. hasFeatures: true,
  137. },
  138. {
  139. plan: PlanDetailsLookupFixture('am2_business'),
  140. features: [features[2]],
  141. hasFeatures: false,
  142. },
  143. ],
  144. });
  145. });
  146. });
  147. it('renders feature list', async function () {
  148. render(
  149. <FeatureList
  150. provider={{key: 'example'}}
  151. organization={organization}
  152. features={features}
  153. />
  154. );
  155. expect(await screen.findByText('Some non-plan feature')).toBeInTheDocument();
  156. expect(screen.getByText('Issue basic plan feature')).toBeInTheDocument();
  157. expect(screen.getByText('Event hooks plan feature')).toBeInTheDocument();
  158. });
  159. it('renders no plan required for `non-plan-feature` feature', async function () {
  160. render(
  161. <FeatureList
  162. provider={{key: 'example'}}
  163. organization={organization}
  164. features={[features[0]!]}
  165. />
  166. );
  167. expect(await screen.findByText('All billing plans')).toBeInTheDocument();
  168. expect(screen.getByText('Some non-plan feature')).toBeInTheDocument();
  169. expect(screen.getByText('Enabled')).toBeInTheDocument();
  170. });
  171. it('renders team plan required for `integrations-issue-basic` feature', async function () {
  172. render(
  173. <FeatureList
  174. provider={{key: 'example'}}
  175. organization={organization}
  176. features={[features[1]!]}
  177. />
  178. );
  179. expect(await screen.findByText('Team billing plans')).toBeInTheDocument();
  180. expect(screen.getByText('Enabled')).toBeInTheDocument();
  181. expect(screen.getByText('Issue basic plan feature')).toBeInTheDocument();
  182. });
  183. it('renders biz plan required for `integrations-event-hooks` feature', async function () {
  184. render(
  185. <FeatureList
  186. provider={{key: 'example'}}
  187. organization={organization}
  188. features={[features[2]!]}
  189. />
  190. );
  191. expect(await screen.findByText('Business billing plans')).toBeInTheDocument();
  192. expect(screen.getByText('Request Trial')).toBeInTheDocument();
  193. expect(screen.getByText('Event hooks plan feature')).toBeInTheDocument();
  194. });
  195. });
  196. describe('Gates features available ONLY on am2 plans', function () {
  197. const features = [
  198. {
  199. description: 'Link stack trace to source code.',
  200. featureGate: 'integrations-stacktrace-link',
  201. },
  202. ];
  203. beforeEach(() => {
  204. MockApiClient.addMockResponse({
  205. url: `/customers/${organization.slug}/billing-config/`,
  206. query: {tier: 'am2'},
  207. body: BillingConfigFixture(PlanTier.AM2),
  208. });
  209. });
  210. it('free features enabled when on any am2 plan', async function () {
  211. organization.features = ['integrations-stacktrace-link'];
  212. const sub = SubscriptionFixture({organization, plan: 'am2_team'});
  213. SubscriptionStore.set(organization.slug, sub);
  214. render(
  215. <FeatureList
  216. provider={{key: 'example'}}
  217. organization={organization}
  218. features={[features[0]!]}
  219. />
  220. );
  221. expect(await screen.findByText('Developer billing plans')).toBeInTheDocument();
  222. expect(screen.getByText('Enabled')).toBeInTheDocument();
  223. expect(screen.getByText('Link stack trace to source code.')).toBeInTheDocument();
  224. });
  225. it('renders required performance plan am1 free features when on legacy plan', async function () {
  226. organization.features = [];
  227. const sub = SubscriptionFixture({organization, plan: 's1', isFree: false});
  228. SubscriptionStore.set(organization.slug, sub);
  229. render(
  230. <FeatureList
  231. provider={{key: 'example'}}
  232. organization={organization}
  233. features={[features[0]!]}
  234. />
  235. );
  236. expect(await screen.findByText('Developer billing plans')).toBeInTheDocument();
  237. expect(screen.queryByText('Enabled')).not.toBeInTheDocument();
  238. expect(screen.getByText('Link stack trace to source code.')).toBeInTheDocument();
  239. });
  240. });
  241. });