Skip to content
Open
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
286 changes: 286 additions & 0 deletions __tests__/gear.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
import { DataSource } from 'typeorm';
import createOrGetConnection from '../src/db';
import {
disposeGraphQLTesting,
GraphQLTestClient,
GraphQLTestingState,
initializeGraphQLTesting,
MockContext,
saveFixtures,
} from './helpers';
import { User } from '../src/entity/user/User';
import { usersFixture } from './fixture/user';
import { DatasetGear } from '../src/entity/dataset/DatasetGear';
import { UserGear } from '../src/entity/user/UserGear';

let con: DataSource;
let state: GraphQLTestingState;
let client: GraphQLTestClient;
let loggedUser: string | null = null;

beforeAll(async () => {
con = await createOrGetConnection();
state = await initializeGraphQLTesting(
() => new MockContext(con, loggedUser),
);
client = state.client;
});

afterAll(() => disposeGraphQLTesting(state));

beforeEach(async () => {
loggedUser = null;
await saveFixtures(con, User, usersFixture);
});

describe('query gear', () => {
const QUERY = `
query Gear($userId: ID!) {
gear(userId: $userId) {
edges {
node {
id
position
gear {
id
name
}
}
}
}
}
`;

it('should return empty list for user with no gear', async () => {
const res = await client.query(QUERY, { variables: { userId: '1' } });
expect(res.data.gear.edges).toEqual([]);
});

it('should return gear ordered by position', async () => {
const gear1 = await con.getRepository(DatasetGear).save({
name: 'MacBook Pro',
nameNormalized: 'macbookpro',
});
const gear2 = await con.getRepository(DatasetGear).save({
name: 'Keyboard',
nameNormalized: 'keyboard',
});

await con.getRepository(UserGear).save([
{ userId: '1', gearId: gear1.id, position: 1 },
{ userId: '1', gearId: gear2.id, position: 0 },
]);

const res = await client.query(QUERY, { variables: { userId: '1' } });
expect(res.data.gear.edges).toHaveLength(2);
expect(res.data.gear.edges[0].node.gear.name).toBe('Keyboard');
expect(res.data.gear.edges[1].node.gear.name).toBe('MacBook Pro');
});
});

describe('mutation addGear', () => {
const MUTATION = `
mutation AddGear($input: AddGearInput!) {
addGear(input: $input) {
id
gear {
name
}
}
}
`;

it('should require authentication', async () => {
const res = await client.mutate(MUTATION, {
variables: { input: { name: 'MacBook Pro' } },
});
expect(res.errors?.[0]?.extensions?.code).toBe('UNAUTHENTICATED');
});

it('should create gear and dataset entry', async () => {
loggedUser = '1';
const res = await client.mutate(MUTATION, {
variables: {
input: {
name: 'MacBook Pro',
},
},
});

expect(res.data.addGear.gear.name).toBe('MacBook Pro');

const dataset = await con
.getRepository(DatasetGear)
.findOneBy({ nameNormalized: 'macbookpro' });
expect(dataset).not.toBeNull();
});

it('should reuse existing dataset entry', async () => {
loggedUser = '1';
await con.getRepository(DatasetGear).save({
name: 'Keyboard',
nameNormalized: 'keyboard',
});

await client.mutate(MUTATION, {
variables: { input: { name: 'Keyboard' } },
});

const count = await con.getRepository(DatasetGear).countBy({
nameNormalized: 'keyboard',
});
expect(count).toBe(1);
});

it('should prevent duplicate gear', async () => {
loggedUser = '1';
await client.mutate(MUTATION, {
variables: { input: { name: 'Monitor' } },
});

const res = await client.mutate(MUTATION, {
variables: { input: { name: 'Monitor' } },
});

expect(res.errors?.[0]?.message).toBe(
'Gear already exists in your profile',
);
});
});

describe('mutation deleteGear', () => {
const MUTATION = `
mutation DeleteGear($id: ID!) {
deleteGear(id: $id) {
_
}
}
`;

it('should require authentication', async () => {
const res = await client.mutate(MUTATION, {
variables: { id: '00000000-0000-0000-0000-000000000000' },
});
expect(res.errors?.[0]?.extensions?.code).toBe('UNAUTHENTICATED');
});

it('should delete gear', async () => {
loggedUser = '1';
const gear = await con.getRepository(DatasetGear).save({
name: 'Webcam',
nameNormalized: 'webcam',
});
const userGear = await con.getRepository(UserGear).save({
userId: '1',
gearId: gear.id,
position: 0,
});

await client.mutate(MUTATION, { variables: { id: userGear.id } });

const deleted = await con
.getRepository(UserGear)
.findOneBy({ id: userGear.id });
expect(deleted).toBeNull();
});

it('should not delete another user gear', async () => {
loggedUser = '1';
const gear = await con.getRepository(DatasetGear).save({
name: 'Mouse',
nameNormalized: 'mouse',
});
const userGear = await con.getRepository(UserGear).save({
userId: '2', // Different user
gearId: gear.id,
position: 0,
});

await client.mutate(MUTATION, { variables: { id: userGear.id } });

// Should still exist because it belongs to user 2
const notDeleted = await con
.getRepository(UserGear)
.findOneBy({ id: userGear.id });
expect(notDeleted).not.toBeNull();
});
});

describe('mutation reorderGear', () => {
const MUTATION = `
mutation ReorderGear($items: [ReorderGearInput!]!) {
reorderGear(items: $items) {
id
position
}
}
`;

it('should require authentication', async () => {
const res = await client.mutate(MUTATION, {
variables: {
items: [{ id: '00000000-0000-0000-0000-000000000000', position: 0 }],
},
});
expect(res.errors?.[0]?.extensions?.code).toBe('UNAUTHENTICATED');
});

it('should update positions', async () => {
loggedUser = '1';
const gear1 = await con.getRepository(DatasetGear).save({
name: 'Desk',
nameNormalized: 'desk',
});
const gear2 = await con.getRepository(DatasetGear).save({
name: 'Chair',
nameNormalized: 'chair',
});

const [item1, item2] = await con.getRepository(UserGear).save([
{ userId: '1', gearId: gear1.id, position: 0 },
{ userId: '1', gearId: gear2.id, position: 1 },
]);

const res = await client.mutate(MUTATION, {
variables: {
items: [
{ id: item1.id, position: 1 },
{ id: item2.id, position: 0 },
],
},
});

const reordered = res.data.reorderGear;
expect(
reordered.find((i: { id: string }) => i.id === item1.id).position,
).toBe(1);
expect(
reordered.find((i: { id: string }) => i.id === item2.id).position,
).toBe(0);
});

it('should not reorder another user gear', async () => {
loggedUser = '1';
const gear = await con.getRepository(DatasetGear).save({
name: 'Headphones',
nameNormalized: 'headphones',
});
const userGear = await con.getRepository(UserGear).save({
userId: '2', // Different user
gearId: gear.id,
position: 0,
});

await client.mutate(MUTATION, {
variables: {
items: [{ id: userGear.id, position: 5 }],
},
});

// Position should still be 0 because it belongs to user 2
const notReordered = await con
.getRepository(UserGear)
.findOneBy({ id: userGear.id });
expect(notReordered?.position).toBe(0);
});
});
33 changes: 33 additions & 0 deletions src/common/datasetGear.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { DataSource } from 'typeorm';
import { DatasetGear } from '../entity/dataset/DatasetGear';

const normalizeName = (name: string): string =>
name
.toLowerCase()
.trim()
.replace(/\./g, 'dot')
.replace(/\+/g, 'plus')
.replace(/#/g, 'sharp')
.replace(/\s+/g, '');

export const findOrCreateDatasetGear = async (
con: DataSource,
name: string,
): Promise<DatasetGear> => {
const nameNormalized = normalizeName(name);
const repo = con.getRepository(DatasetGear);

let gear = await repo.findOne({
where: { nameNormalized },
});

if (!gear) {
gear = repo.create({
name: name.trim(),
nameNormalized,
});
await repo.save(gear);
}

return gear;
};
7 changes: 7 additions & 0 deletions src/common/schema/autocompletes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,10 @@ export const autocompleteGithubRepositorySchema = z.object({
query: z.string().trim().min(1).max(100),
limit: z.number().min(1).max(20).default(10),
});

export const autocompleteGearSchema = z.object({
query: z
.string()
.min(1)
.transform((v) => v.trim().toLowerCase()),
});
15 changes: 15 additions & 0 deletions src/common/schema/gear.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import z from 'zod';

export const addGearSchema = z.object({
name: z.string().min(1).max(255),
});

export const reorderGearItemSchema = z.object({
id: z.uuid(),
position: z.number().int().min(0),
});

export const reorderGearSchema = z.array(reorderGearItemSchema).min(1);

export type AddGearInput = z.infer<typeof addGearSchema>;
export type ReorderGearInput = z.infer<typeof reorderGearItemSchema>;
23 changes: 23 additions & 0 deletions src/graphorm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2244,6 +2244,29 @@ const obj = new GraphORM({
},
},
},
DatasetGear: {
requiredColumns: ['id', 'name'],
fields: {
createdAt: {
transform: transformDate,
},
},
},
Gear: {
requiredColumns: ['id', 'userId', 'gearId'],
fields: {
gear: {
relation: {
isMany: false,
childColumn: 'id',
parentColumn: 'gearId',
},
},
createdAt: {
transform: transformDate,
},
},
},
});

export default obj;
Loading
Loading