import {initializeOrg} from 'sentry-test/initializeOrg';
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
import {DEFAULT_STATS_PERIOD} from 'sentry/constants';
import OrganizationStore from 'sentry/stores/organizationStore';
import PageFiltersStore from 'sentry/stores/pageFiltersStore';
import ProjectsStore from 'sentry/stores/projectsStore';
import {DataCategory, PageFilters} from 'sentry/types';
import {OrganizationStats, PAGE_QUERY_PARAMS} from 'sentry/views/organizationStats';
import {ChartDataTransform} from './usageChart';
describe('OrganizationStats', function () {
const defaultSelection: PageFilters = {
projects: [],
environments: [],
datetime: {
start: null,
end: null,
period: '24h',
utc: false,
},
};
const projects = ['1', '2', '3'].map(id => TestStubs.Project({id, slug: `proj-${id}`}));
const {organization, router, routerContext} = initializeOrg({
organization: {features: ['global-views', 'team-insights']},
projects,
project: undefined,
router: undefined,
});
const endpoint = `/organizations/${organization.slug}/stats_v2/`;
const defaultProps: OrganizationStats['props'] = {
router,
organization,
...router,
route: {},
params: {orgId: organization.slug as string},
routeParams: {},
};
let mockRequest;
beforeEach(() => {
MockApiClient.clearMockResponses();
PageFiltersStore.init();
PageFiltersStore.onInitializeUrlState(defaultSelection, new Set());
OrganizationStore.onUpdate(organization, {replace: true});
ProjectsStore.loadInitialData(projects);
mockRequest = MockApiClient.addMockResponse({
method: 'GET',
url: endpoint,
body: mockStatsResponse,
});
});
afterEach(() => {
PageFiltersStore.reset();
});
/**
* Features and Alerts
*/
it('renders header state wihout tabs', () => {
const newOrg = initializeOrg();
const newProps = {
...defaultProps,
organization: newOrg.organization,
};
render(, {context: newOrg.routerContext});
expect(screen.getByText('Organization Usage Stats')).toBeInTheDocument();
});
it('renders header state with tabs', () => {
render(, {context: routerContext});
expect(screen.getByText('Stats')).toBeInTheDocument();
expect(screen.getByText('Usage')).toBeInTheDocument();
expect(screen.getByText('Issues')).toBeInTheDocument();
expect(screen.getByText('Health')).toBeInTheDocument();
});
/**
* Base + Error Handling
*/
it('renders the base view', () => {
render(, {context: routerContext});
// Default to Errors category
expect(screen.getAllByText('Errors')[0]).toBeInTheDocument();
// Render the chart and project table
expect(screen.getByTestId('usage-stats-chart')).toBeInTheDocument();
expect(screen.getByTestId('usage-stats-table')).toBeInTheDocument();
// Render the cards
expect(screen.getAllByText('Total')[0]).toBeInTheDocument();
expect(screen.getByText('64')).toBeInTheDocument();
expect(screen.getAllByText('Accepted')[0]).toBeInTheDocument();
expect(screen.getByText('28')).toBeInTheDocument();
expect(screen.getByText('6 in last min')).toBeInTheDocument();
expect(screen.getAllByText('Filtered')[0]).toBeInTheDocument();
expect(screen.getAllByText('7')[0]).toBeInTheDocument();
expect(screen.getAllByText('Dropped')[0]).toBeInTheDocument();
expect(screen.getAllByText('29')[0]).toBeInTheDocument();
// Correct API Calls
const mockExpectations = {
UsageStatsOrg: {
statsPeriod: DEFAULT_STATS_PERIOD,
interval: '1h',
groupBy: ['category', 'outcome'],
field: ['sum(quantity)'],
},
UsageStatsPerMin: {
statsPeriod: '5m',
interval: '1m',
groupBy: ['category', 'outcome'],
field: ['sum(quantity)'],
},
UsageStatsProjects: {
statsPeriod: DEFAULT_STATS_PERIOD,
interval: '1h',
groupBy: ['outcome', 'project'],
project: '-1',
field: ['sum(quantity)'],
category: 'error',
},
};
for (const query of Object.values(mockExpectations)) {
expect(mockRequest).toHaveBeenCalledWith(
endpoint,
expect.objectContaining({query})
);
}
});
it('renders with an error on stats endpoint', () => {
MockApiClient.clearMockResponses();
MockApiClient.addMockResponse({
url: endpoint,
statusCode: 500,
});
render(, {context: routerContext});
expect(screen.getByTestId('usage-stats-chart')).toBeInTheDocument();
expect(screen.getByTestId('usage-stats-table')).toBeInTheDocument();
expect(screen.getByTestId('error-messages')).toBeInTheDocument();
});
it('renders with an error when user has no projects', () => {
MockApiClient.clearMockResponses();
MockApiClient.addMockResponse({
url: endpoint,
statusCode: 400,
body: {detail: 'No projects available'},
});
render(, {context: routerContext});
expect(screen.getByTestId('usage-stats-chart')).toBeInTheDocument();
expect(screen.getByTestId('usage-stats-table')).toBeInTheDocument();
expect(screen.getByTestId('empty-message')).toBeInTheDocument();
});
/**
* Router Handling
*/
it('pushes state changes to the route', () => {
render(, {context: routerContext});
userEvent.click(screen.getByText('Category'));
userEvent.click(screen.getByText('Attachments'));
expect(router.push).toHaveBeenCalledWith(
expect.objectContaining({
query: {dataCategory: DataCategory.ATTACHMENTS},
})
);
userEvent.click(screen.getByText('Periodic'));
userEvent.click(screen.getByText('Cumulative'));
expect(router.push).toHaveBeenCalledWith(
expect.objectContaining({
query: {transform: ChartDataTransform.CUMULATIVE},
})
);
const inputQuery = 'proj-1';
userEvent.type(
screen.getByPlaceholderText('Filter your projects'),
`${inputQuery}{enter}`
);
expect(router.push).toHaveBeenCalledWith(
expect.objectContaining({
query: {query: inputQuery},
})
);
});
it('does not leak query params onto next page links', () => {
const dummyLocation = PAGE_QUERY_PARAMS.reduce(
(location, param) => {
location.query[param] = '';
return location;
},
{query: {}}
);
const newProps = {
...defaultProps,
location: dummyLocation as any,
};
render(, {context: routerContext});
const projectLinks = screen.getAllByTestId('badge-display-name');
expect(projectLinks.length).toBeGreaterThan(0);
const leakingRegex = PAGE_QUERY_PARAMS.join('|');
for (const projectLink of projectLinks) {
expect(projectLink.closest('a')).toHaveAttribute(
'href',
expect.not.stringMatching(leakingRegex)
);
}
});
});
const mockStatsResponse = {
start: '2021-01-01T00:00:00Z',
end: '2021-01-07T00:00:00Z',
intervals: [
'2021-01-01T00:00:00Z',
'2021-01-02T00:00:00Z',
'2021-01-03T00:00:00Z',
'2021-01-04T00:00:00Z',
'2021-01-05T00:00:00Z',
'2021-01-06T00:00:00Z',
'2021-01-07T00:00:00Z',
],
groups: [
{
by: {
project: 1,
category: 'attachment',
outcome: 'accepted',
},
totals: {
'sum(quantity)': 28000,
},
series: {
'sum(quantity)': [1000, 2000, 3000, 4000, 5000, 6000, 7000],
},
},
{
by: {
project: 1,
outcome: 'accepted',
category: 'transaction',
},
totals: {
'sum(quantity)': 28,
},
series: {
'sum(quantity)': [1, 2, 3, 4, 5, 6, 7],
},
},
{
by: {
project: 1,
category: 'error',
outcome: 'accepted',
},
totals: {
'sum(quantity)': 28,
},
series: {
'sum(quantity)': [1, 2, 3, 4, 5, 6, 7],
},
},
{
by: {
project: 1,
category: 'error',
outcome: 'filtered',
},
totals: {
'sum(quantity)': 7,
},
series: {
'sum(quantity)': [1, 1, 1, 1, 1, 1, 1],
},
},
{
by: {
project: 1,
category: 'error',
outcome: 'rate_limited',
},
totals: {
'sum(quantity)': 14,
},
series: {
'sum(quantity)': [2, 2, 2, 2, 2, 2, 2],
},
},
{
by: {
project: 1,
category: 'error',
outcome: 'invalid',
},
totals: {
'sum(quantity)': 15,
},
series: {
'sum(quantity)': [2, 2, 2, 2, 2, 2, 3],
},
},
{
by: {
project: 1,
category: 'error',
outcome: 'client_discard',
},
totals: {
'sum(quantity)': 15,
},
series: {
'sum(quantity)': [2, 2, 2, 2, 2, 2, 3],
},
},
],
};