feat: možnost označení návrhu jako vyřešeného (resolved)
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 36s
CI / Playwright E2E tests (push) Successful in 1m18s
CI / Build and push Docker image (push) Successful in 41s
CI / Notify (push) Successful in 2s

This commit is contained in:
2026-06-10 18:38:55 +02:00
parent 491ec25b52
commit cc09ddbd2c
6 changed files with 165 additions and 52 deletions
+6
View File
@@ -13,6 +13,8 @@ interface StoredSuggestion {
upvoters: string[];
/** Loginy uživatelů hlasujících PROTI návrhu */
downvoters: string[];
/** Příznak vyřešeného (zapracovaného) návrhu - nastavuje se pouze ručním zásahem do dat */
resolved?: boolean;
}
const storage = getStorage();
@@ -42,6 +44,7 @@ function toDto(suggestion: StoredSuggestion, login: string): Suggestion {
voteScore: suggestion.upvoters.length - suggestion.downvoters.length,
myVote,
isMine: suggestion.author === login,
resolved: suggestion.resolved ?? false,
};
}
@@ -105,6 +108,9 @@ export async function voteSuggestion(login: string, id: string, direction: VoteD
if (!suggestion) {
throw new Error('Návrh nebyl nalezen');
}
if (suggestion.resolved) {
throw new Error('Pro vyřešený návrh nelze hlasovat');
}
const hadUp = suggestion.upvoters.includes(login);
const hadDown = suggestion.downvoters.includes(login);
// Nejprve odebereme případný stávající hlas uživatele
+39
View File
@@ -1,6 +1,15 @@
import { resetMemoryStorage } from '../storage/memory';
import getStorage from '../storage';
import { addSuggestion, deleteSuggestion, listSuggestions, voteSuggestion } from '../suggestions';
/** Ručně označí návrh jako vyřešený - simuluje zásah do dat (Redis/JSON). */
async function markResolved(id: string) {
const storage = getStorage();
const data = await storage.getData<any[]>('suggestions');
data!.find(s => s.id === id).resolved = true;
await storage.setData('suggestions', data);
}
const AUTHOR = 'tomas';
const VOTER = 'petr';
const OTHER = 'jana';
@@ -106,6 +115,36 @@ describe('voteSuggestion', () => {
});
});
describe('vyřešené návrhy', () => {
test('listSuggestions vrací příznak resolved', async () => {
const id = await createSuggestion();
expect((await listSuggestions(AUTHOR))[0].resolved).toBe(false);
await markResolved(id);
expect((await listSuggestions(AUTHOR))[0].resolved).toBe(true);
});
test('pro vyřešený návrh nelze hlasovat', async () => {
const id = await createSuggestion();
await markResolved(id);
await expect(voteSuggestion(VOTER, id, 'up')).rejects.toThrow();
// skóre zůstává nezměněné (jen autorův hlas)
expect((await listSuggestions(VOTER))[0].voteScore).toBe(1);
});
test('autor může vyřešený návrh stále smazat', async () => {
const id = await createSuggestion();
await markResolved(id);
const list = await deleteSuggestion(AUTHOR, id);
expect(list).toHaveLength(0);
});
test('cizí uživatel nemůže smazat ani vyřešený návrh', async () => {
const id = await createSuggestion();
await markResolved(id);
await expect(deleteSuggestion(VOTER, id)).rejects.toThrow();
});
});
describe('deleteSuggestion', () => {
test('autor smaže svůj návrh včetně hlasů', async () => {
const id = await createSuggestion();