feat: nová stránka pro návrhy na vylepšení
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 21s
CI / Build server (push) Successful in 24s
CI / Build client (push) Successful in 38s
CI / Playwright E2E tests (push) Successful in 1m18s
CI / Build and push Docker image (push) Successful in 40s
CI / Notify (push) Successful in 2s
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 21s
CI / Build server (push) Successful in 24s
CI / Build client (push) Successful in 38s
CI / Playwright E2E tests (push) Successful in 1m18s
CI / Build and push Docker image (push) Successful in 40s
CI / Notify (push) Successful in 2s
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import { resetMemoryStorage } from '../storage/memory';
|
||||
import { addSuggestion, deleteSuggestion, listSuggestions, voteSuggestion } from '../suggestions';
|
||||
|
||||
const AUTHOR = 'tomas';
|
||||
const VOTER = 'petr';
|
||||
const OTHER = 'jana';
|
||||
|
||||
beforeEach(() => {
|
||||
resetMemoryStorage();
|
||||
});
|
||||
|
||||
/** Pomocná funkce: vytvoří návrh a vrátí jeho id (z pohledu autora). */
|
||||
async function createSuggestion(author = AUTHOR, title = 'Tmavý režim', description = 'Přidat tmavý režim aplikace') {
|
||||
const list = await addSuggestion(author, title, description);
|
||||
return list.find(s => s.title === title)!.id;
|
||||
}
|
||||
|
||||
describe('addSuggestion', () => {
|
||||
test('přidá návrh a autorovi nastaví hlas pro', async () => {
|
||||
const list = await addSuggestion(AUTHOR, 'Tmavý režim', 'Popis');
|
||||
expect(list).toHaveLength(1);
|
||||
const s = list[0];
|
||||
expect(s.author).toBe(AUTHOR);
|
||||
expect(s.title).toBe('Tmavý režim');
|
||||
expect(s.description).toBe('Popis');
|
||||
expect(s.voteScore).toBe(1);
|
||||
expect(s.myVote).toBe('up');
|
||||
expect(s.isMine).toBe(true);
|
||||
});
|
||||
|
||||
test('ořízne mezery a odmítne prázdný název i popis', async () => {
|
||||
await expect(addSuggestion(AUTHOR, ' ', 'popis')).rejects.toThrow();
|
||||
await expect(addSuggestion(AUTHOR, 'název', ' ')).rejects.toThrow();
|
||||
const list = await addSuggestion(AUTHOR, ' Název ', ' Popis ');
|
||||
expect(list[0].title).toBe('Název');
|
||||
expect(list[0].description).toBe('Popis');
|
||||
});
|
||||
|
||||
test('jeden uživatel může přidat více návrhů', async () => {
|
||||
await addSuggestion(AUTHOR, 'První', 'popis');
|
||||
const list = await addSuggestion(AUTHOR, 'Druhý', 'popis');
|
||||
expect(list).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('listSuggestions', () => {
|
||||
test('řadí sestupně dle skóre', async () => {
|
||||
const lowId = await createSuggestion(AUTHOR, 'Nízké', 'popis');
|
||||
const highId = await createSuggestion(OTHER, 'Vysoké', 'popis');
|
||||
// "Vysoké" dostane další hlas pro
|
||||
await voteSuggestion(VOTER, highId, 'up');
|
||||
|
||||
const list = await listSuggestions(VOTER);
|
||||
expect(list[0].id).toBe(highId);
|
||||
expect(list[0].voteScore).toBe(2);
|
||||
expect(list[1].id).toBe(lowId);
|
||||
});
|
||||
|
||||
test('myVote a isMine jsou relativní k uživateli', async () => {
|
||||
const id = await createSuggestion(AUTHOR);
|
||||
const asAuthor = (await listSuggestions(AUTHOR))[0];
|
||||
expect(asAuthor.isMine).toBe(true);
|
||||
expect(asAuthor.myVote).toBe('up');
|
||||
|
||||
const asOther = (await listSuggestions(VOTER))[0];
|
||||
expect(asOther.isMine).toBe(false);
|
||||
expect(asOther.myVote).toBeUndefined();
|
||||
// Seznamy hlasujících se klientovi neposílají
|
||||
expect((asOther as any).upvoters).toBeUndefined();
|
||||
expect(id).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('voteSuggestion', () => {
|
||||
test('hlas pro a proti od jiného uživatele', async () => {
|
||||
const id = await createSuggestion();
|
||||
let list = await voteSuggestion(VOTER, id, 'up');
|
||||
expect(list[0].voteScore).toBe(2);
|
||||
expect(list.find(s => s.id === id)!.voteScore).toBe(2);
|
||||
|
||||
// přehlasování z pro na proti
|
||||
list = await voteSuggestion(VOTER, id, 'down');
|
||||
// autor +1, voter -1 => 0
|
||||
expect(list[0].voteScore).toBe(0);
|
||||
expect((await listSuggestions(VOTER))[0].myVote).toBe('down');
|
||||
});
|
||||
|
||||
test('klik na aktivní směr hlas zruší', async () => {
|
||||
const id = await createSuggestion();
|
||||
await voteSuggestion(VOTER, id, 'up');
|
||||
const list = await voteSuggestion(VOTER, id, 'up');
|
||||
// zůstává jen autorův hlas
|
||||
expect(list[0].voteScore).toBe(1);
|
||||
expect((await listSuggestions(VOTER))[0].myVote).toBeUndefined();
|
||||
});
|
||||
|
||||
test('autor může svůj automatický hlas odebrat', async () => {
|
||||
const id = await createSuggestion();
|
||||
const list = await voteSuggestion(AUTHOR, id, 'up');
|
||||
expect(list[0].voteScore).toBe(0);
|
||||
expect((await listSuggestions(AUTHOR))[0].myVote).toBeUndefined();
|
||||
});
|
||||
|
||||
test('hlasování pro neexistující návrh vyhodí chybu', async () => {
|
||||
await expect(voteSuggestion(VOTER, 'neexistuje', 'up')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteSuggestion', () => {
|
||||
test('autor smaže svůj návrh včetně hlasů', async () => {
|
||||
const id = await createSuggestion();
|
||||
await voteSuggestion(VOTER, id, 'up');
|
||||
const list = await deleteSuggestion(AUTHOR, id);
|
||||
expect(list).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('cizí uživatel nemůže smazat návrh', async () => {
|
||||
const id = await createSuggestion();
|
||||
await expect(deleteSuggestion(VOTER, id)).rejects.toThrow();
|
||||
expect(await listSuggestions(AUTHOR)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('smazání neexistujícího návrhu vyhodí chybu', async () => {
|
||||
await expect(deleteSuggestion(AUTHOR, 'neexistuje')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -1,77 +0,0 @@
|
||||
import { getUserVotes, updateFeatureVote, getVotingStats } from '../voting';
|
||||
import { resetMemoryStorage } from '../storage/memory';
|
||||
import { FeatureRequest } from '../../../types/gen/types.gen';
|
||||
|
||||
const OPT_A = FeatureRequest.STATISTICS;
|
||||
const OPT_B = FeatureRequest.UI;
|
||||
|
||||
beforeEach(() => {
|
||||
resetMemoryStorage();
|
||||
});
|
||||
|
||||
describe('updateFeatureVote', () => {
|
||||
test('přidá hlas pro nového uživatele', async () => {
|
||||
const result = await updateFeatureVote('alice', OPT_A, true);
|
||||
expect(result['alice']).toContain(OPT_A);
|
||||
});
|
||||
|
||||
test('vyhodí chybu při duplicitním hlasování', async () => {
|
||||
await updateFeatureVote('alice', OPT_A, true);
|
||||
await expect(updateFeatureVote('alice', OPT_A, true)).rejects.toThrow('hlasovali');
|
||||
});
|
||||
|
||||
test('odebere hlas', async () => {
|
||||
await updateFeatureVote('alice', OPT_A, true);
|
||||
await updateFeatureVote('alice', OPT_A, false);
|
||||
const stats = await getVotingStats();
|
||||
expect(stats[OPT_A] ?? 0).toBe(0);
|
||||
});
|
||||
|
||||
test('odebrání neexistujícího hlasu je no-op', async () => {
|
||||
await expect(updateFeatureVote('alice', OPT_A, false)).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
test('odebrání posledního hlasu odstraní login ze storage', async () => {
|
||||
await updateFeatureVote('alice', OPT_A, true);
|
||||
const data = await updateFeatureVote('alice', OPT_A, false);
|
||||
expect('alice' in data).toBe(false);
|
||||
});
|
||||
|
||||
test('vyhodí chybu po 4 hlasech', async () => {
|
||||
const options = Object.values(FeatureRequest);
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await updateFeatureVote('alice', options[i], true);
|
||||
}
|
||||
await expect(updateFeatureVote('alice', options[4], true)).rejects.toThrow('4');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserVotes', () => {
|
||||
test('vrátí hlasy uživatele', async () => {
|
||||
await updateFeatureVote('alice', OPT_A, true);
|
||||
const votes = await getUserVotes('alice');
|
||||
expect(votes).toContain(OPT_A);
|
||||
});
|
||||
|
||||
test('vrátí prázdné pole pro uživatele bez hlasů', async () => {
|
||||
const votes = await getUserVotes('neexistujici');
|
||||
expect(votes).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVotingStats', () => {
|
||||
test('vrátí agregované počty hlasů', async () => {
|
||||
await updateFeatureVote('alice', OPT_A, true);
|
||||
await updateFeatureVote('bob', OPT_A, true);
|
||||
await updateFeatureVote('bob', OPT_B, true);
|
||||
|
||||
const stats = await getVotingStats();
|
||||
expect(stats[OPT_A]).toBe(2);
|
||||
expect(stats[OPT_B]).toBe(1);
|
||||
});
|
||||
|
||||
test('vrátí prázdný objekt bez hlasů', async () => {
|
||||
const stats = await getVotingStats();
|
||||
expect(stats).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -1,76 +0,0 @@
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
import bodyParser from 'body-parser';
|
||||
import { generateToken } from '../auth';
|
||||
import { resetMemoryStorage } from '../storage/memory';
|
||||
import { FeatureRequest } from '../../../types/gen/types.gen';
|
||||
import votingRouter from '../routes/votingRoutes';
|
||||
|
||||
const VALID_OPTION = FeatureRequest.STATISTICS;
|
||||
|
||||
function buildApp() {
|
||||
const app = express();
|
||||
app.use(bodyParser.json());
|
||||
app.use('/api/voting', votingRouter);
|
||||
app.use((err: any, _req: any, res: any, _next: any) => {
|
||||
res.status(400).json({ error: err.message });
|
||||
});
|
||||
return app;
|
||||
}
|
||||
|
||||
const TOKEN = `Bearer ${generateToken('testuser')}`;
|
||||
|
||||
beforeEach(() => {
|
||||
resetMemoryStorage();
|
||||
});
|
||||
|
||||
test('GET /getVotes vrátí 200 s prázdným polem pro nového uživatele', async () => {
|
||||
const res = await request(buildApp())
|
||||
.get('/api/voting/getVotes')
|
||||
.set('Authorization', TOKEN);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual([]);
|
||||
});
|
||||
|
||||
test('GET /getVotes vrátí 401 bez tokenu', async () => {
|
||||
const res = await request(buildApp()).get('/api/voting/getVotes');
|
||||
expect(res.status).toBeGreaterThanOrEqual(400);
|
||||
});
|
||||
|
||||
test('POST /updateVote přidá hlas a vrátí 200', async () => {
|
||||
const res = await request(buildApp())
|
||||
.post('/api/voting/updateVote')
|
||||
.set('Authorization', TOKEN)
|
||||
.send({ option: VALID_OPTION, active: true });
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
test('POST /updateVote vrátí 400 pro chybějící parametry', async () => {
|
||||
const res = await request(buildApp())
|
||||
.post('/api/voting/updateVote')
|
||||
.set('Authorization', TOKEN)
|
||||
.send({});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
test('POST /updateVote vrátí 400 při duplicitním hlasu', async () => {
|
||||
const app = buildApp();
|
||||
await request(app)
|
||||
.post('/api/voting/updateVote')
|
||||
.set('Authorization', TOKEN)
|
||||
.send({ option: VALID_OPTION, active: true });
|
||||
const res = await request(app)
|
||||
.post('/api/voting/updateVote')
|
||||
.set('Authorization', TOKEN)
|
||||
.send({ option: VALID_OPTION, active: true });
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain('hlasovali');
|
||||
});
|
||||
|
||||
test('GET /stats vrátí 200 s objektem', async () => {
|
||||
const res = await request(buildApp())
|
||||
.get('/api/voting/stats')
|
||||
.set('Authorization', TOKEN);
|
||||
expect(res.status).toBe(200);
|
||||
expect(typeof res.body).toBe('object');
|
||||
});
|
||||
Reference in New Issue
Block a user