console/packages/libraries/client/tests/usage.spec.ts
Dimitri POSTOLOV de7ba835e4
migrate to vitest (#921)
Co-authored-by: Kamil Kisiela <kamil.kisiela@gmail.com>
2023-02-10 11:11:23 +01:00

411 lines
11 KiB
TypeScript

import { buildSchema, parse } from 'graphql';
// eslint-disable-next-line import/no-extraneous-dependencies
import nock from 'nock';
import { createHive } from '../src/client';
import type { Report } from '../src/internal/usage';
import { version } from '../src/version';
import { waitFor } from './test-utils';
const headers = {
'Content-Type': 'application/json',
'graphql-client-name': 'Hive Client',
'graphql-client-version': version,
};
const schema = buildSchema(/* GraphQL */ `
type Query {
project(selector: ProjectSelectorInput!): Project
projectsByType(type: ProjectType!): [Project!]!
projects(filter: FilterInput): [Project!]!
}
type Mutation {
deleteProject(selector: ProjectSelectorInput!): DeleteProjectPayload!
}
input ProjectSelectorInput {
organization: ID!
project: ID!
}
input FilterInput {
type: ProjectType
pagination: PaginationInput
}
input PaginationInput {
limit: Int
offset: Int
}
type ProjectSelector {
organization: ID!
project: ID!
}
type DeleteProjectPayload {
selector: ProjectSelector!
deletedProject: Project!
}
type Project {
id: ID!
cleanId: ID!
name: String!
type: ProjectType!
buildUrl: String
validationUrl: String
}
enum ProjectType {
FEDERATION
STITCHING
SINGLE
CUSTOM
}
`);
const op = parse(/* GraphQL */ `
mutation deleteProject($selector: ProjectSelectorInput!) {
deleteProject(selector: $selector) {
selector {
organization
project
}
deletedProject {
...ProjectFields
}
}
}
fragment ProjectFields on Project {
id
cleanId
name
type
}
`);
beforeEach(() => {
vi.restoreAllMocks();
});
afterEach(() => {
nock.cleanAll();
});
test('should send data to Hive', async () => {
const logger = {
error: vi.fn(),
info: vi.fn(),
};
const token = 'Token';
let report: Report = {
size: 0,
map: {},
operations: [],
};
const http = nock('http://localhost')
.post('/200')
.matchHeader('Authorization', `Bearer ${token}`)
.matchHeader('Content-Type', headers['Content-Type'])
.matchHeader('graphql-client-name', headers['graphql-client-name'])
.matchHeader('graphql-client-version', headers['graphql-client-version'])
.once()
.reply((_, _body) => {
report = _body as any;
return [200];
});
const hive = createHive({
enabled: true,
debug: true,
agent: {
timeout: 500,
maxRetries: 0,
logger,
},
token,
selfHosting: {
graphqlEndpoint: 'http://localhost/graphql',
applicationUrl: 'http://localhost/',
usageEndpoint: 'http://localhost/200',
},
usage: true,
});
const collect = hive.collectUsage({
schema,
document: op,
operationName: 'deleteProject',
});
await waitFor(2000);
collect({});
await hive.dispose();
await waitFor(1000);
http.done();
expect(logger.error).not.toHaveBeenCalled();
expect(logger.info).toHaveBeenCalledWith(`[hive][usage] Sending (queue 1) (attempt 1)`);
expect(logger.info).toHaveBeenCalledWith(`[hive][usage] Sent!`);
// Map
expect(report.size).toEqual(1);
expect(Object.keys(report.map)).toHaveLength(1);
const key = Object.keys(report.map)[0];
const record = report.map[key];
// operation
expect(record.operation).toMatch('mutation deleteProject');
expect(record.operationName).toMatch('deleteProject');
// fields
expect(record.fields).toHaveLength(13);
expect(record.fields).toContainEqual('Mutation.deleteProject');
expect(record.fields).toContainEqual('Mutation.deleteProject.selector');
expect(record.fields).toContainEqual('DeleteProjectPayload.selector');
expect(record.fields).toContainEqual('ProjectSelector.organization');
expect(record.fields).toContainEqual('ProjectSelector.project');
expect(record.fields).toContainEqual('DeleteProjectPayload.deletedProject');
expect(record.fields).toContainEqual('Project.id');
expect(record.fields).toContainEqual('Project.cleanId');
expect(record.fields).toContainEqual('Project.name');
expect(record.fields).toContainEqual('Project.type');
expect(record.fields).toContainEqual('ProjectSelectorInput.organization');
expect(record.fields).toContainEqual('ID');
expect(record.fields).toContainEqual('ProjectSelectorInput.project');
// Operations
const operations = report.operations;
expect(operations).toHaveLength(1); // one operation
const operation = operations[0];
expect(operation.operationMapKey).toEqual(key);
expect(operation.timestamp).toEqual(expect.any(Number));
// execution
expect(operation.execution.duration).toBeGreaterThanOrEqual(1500 * 1_000_000); // >=1500ms in microseconds
expect(operation.execution.duration).toBeLessThan(3000 * 1_000_000); // <3000ms
expect(operation.execution.errorsTotal).toBe(0);
expect(operation.execution.errors).toHaveLength(0);
expect(operation.execution.ok).toBe(true);
});
test('should send data to Hive (deprecated endpoint)', async () => {
const logger = {
error: vi.fn(),
info: vi.fn(),
};
const token = 'Token';
let report: Report = {
size: 0,
map: {},
operations: [],
};
const http = nock('http://localhost')
.post('/200')
.matchHeader('Authorization', `Bearer ${token}`)
.matchHeader('Content-Type', headers['Content-Type'])
.matchHeader('graphql-client-name', headers['graphql-client-name'])
.matchHeader('graphql-client-version', headers['graphql-client-version'])
.once()
.reply((_, _body) => {
report = _body as any;
return [200];
});
const hive = createHive({
enabled: true,
debug: true,
agent: {
timeout: 500,
maxRetries: 0,
logger,
},
token,
usage: {
endpoint: 'http://localhost/200',
},
});
const collect = hive.collectUsage({
schema,
document: op,
operationName: 'deleteProject',
});
await waitFor(2000);
collect({});
await hive.dispose();
await waitFor(1000);
http.done();
expect(logger.error).not.toHaveBeenCalled();
expect(logger.info).toHaveBeenCalledWith(`[hive][usage] Sending (queue 1) (attempt 1)`);
expect(logger.info).toHaveBeenCalledWith(`[hive][usage] Sent!`);
// Map
expect(report.size).toEqual(1);
expect(Object.keys(report.map)).toHaveLength(1);
const key = Object.keys(report.map)[0];
const record = report.map[key];
// operation
expect(record.operation).toMatch('mutation deleteProject');
expect(record.operationName).toMatch('deleteProject');
// fields
expect(record.fields).toHaveLength(13);
expect(record.fields).toContainEqual('Mutation.deleteProject');
expect(record.fields).toContainEqual('Mutation.deleteProject.selector');
expect(record.fields).toContainEqual('DeleteProjectPayload.selector');
expect(record.fields).toContainEqual('ProjectSelector.organization');
expect(record.fields).toContainEqual('ProjectSelector.project');
expect(record.fields).toContainEqual('DeleteProjectPayload.deletedProject');
expect(record.fields).toContainEqual('Project.id');
expect(record.fields).toContainEqual('Project.cleanId');
expect(record.fields).toContainEqual('Project.name');
expect(record.fields).toContainEqual('Project.type');
expect(record.fields).toContainEqual('ProjectSelectorInput.organization');
expect(record.fields).toContainEqual('ID');
expect(record.fields).toContainEqual('ProjectSelectorInput.project');
// Operations
const operations = report.operations;
expect(operations).toHaveLength(1); // one operation
const operation = operations[0];
expect(operation.operationMapKey).toEqual(key);
expect(operation.timestamp).toEqual(expect.any(Number));
// execution
expect(operation.execution.duration).toBeGreaterThanOrEqual(1500 * 1_000_000); // >=1500ms in microseconds
expect(operation.execution.duration).toBeLessThan(3000 * 1_000_000); // <3000ms
expect(operation.execution.errorsTotal).toBe(0);
expect(operation.execution.errors).toHaveLength(0);
expect(operation.execution.ok).toBe(true);
});
test('should not leak the exception', async () => {
const logger = {
error: vi.fn(),
info: vi.fn(),
};
const hive = createHive({
enabled: true,
debug: true,
agent: {
timeout: 500,
maxRetries: 1,
logger,
},
token: 'Token',
usage: {
endpoint: 'http://404.localhost',
},
});
hive.collectUsage({
schema,
document: op,
operationName: 'deleteProject',
})({});
await waitFor(1000);
await hive.dispose();
expect(logger.info).toHaveBeenCalledWith(`[hive][usage] Sending (queue 1) (attempt 1)`);
expect(logger.info).toHaveBeenCalledWith(
expect.stringContaining(`[hive][usage] Attempt 1 failed:`),
);
expect(logger.info).toHaveBeenCalledWith(`[hive][usage] Sending (queue 1) (attempt 2)`);
expect(logger.error).toHaveBeenCalledTimes(1);
expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining(`[hive][usage] Failed to send data`),
);
});
test('sendImmediately should not stop the schedule', async () => {
const logger = {
error: vi.fn(),
info: vi.fn(),
};
const token = 'Token';
const http = nock('http://localhost')
.post('/200')
.matchHeader('authorization', `Bearer ${token}`)
.matchHeader('Content-Type', headers['Content-Type'])
.matchHeader('graphql-client-name', headers['graphql-client-name'])
.matchHeader('graphql-client-version', headers['graphql-client-version'])
.times(3)
.reply((_, _body) => {
return [200];
});
const hive = createHive({
enabled: true,
debug: true,
agent: {
timeout: 500,
maxRetries: 0,
maxSize: 2,
logger,
sendInterval: 100,
},
token,
usage: {
endpoint: 'http://localhost/200',
},
});
const collect = hive.collectUsage({
schema,
document: op,
operationName: 'deleteProject',
});
expect(logger.info).toHaveBeenCalledTimes(0);
collect({});
await waitFor(200);
// Because maxSize is 2 and sendInterval is 100ms
// the scheduled send task should be done by now
expect(logger.error).not.toHaveBeenCalled();
expect(logger.info).toHaveBeenCalledWith(`[hive][usage] Sending (queue 1) (attempt 1)`);
expect(logger.info).toHaveBeenCalledWith(`[hive][usage] Sent!`);
expect(logger.info).not.toHaveBeenCalledWith(`[hive][usage] Sending immediately`);
expect(logger.info).toHaveBeenCalledTimes(2);
// Now we will check the maxSize
// We run collect three times
collect({});
collect({});
expect(logger.error).not.toHaveBeenCalled();
expect(logger.info).toHaveBeenCalledWith(`[hive][usage] Sending (queue 1) (attempt 1)`);
expect(logger.info).toHaveBeenCalledWith(`[hive][usage] Sending immediately`);
await waitFor(1); // we run setImmediate under the hood
// It should be sent already
expect(logger.info).toHaveBeenCalledWith(`[hive][usage] Sent!`);
expect(logger.info).toHaveBeenCalledTimes(4);
await waitFor(50);
expect(logger.info).toHaveBeenCalledTimes(5);
// Let's check if the scheduled send task is still running
collect({});
await waitFor(200);
expect(logger.error).not.toHaveBeenCalled();
expect(logger.info).toHaveBeenCalledWith(`[hive][usage] Sending (queue 1) (attempt 1)`);
expect(logger.info).toHaveBeenCalledWith(`[hive][usage] Sent!`);
expect(logger.info).toHaveBeenCalledTimes(7);
await hive.dispose();
await waitFor(1000);
http.done();
});