diff --git a/apiTest/PotterAPI/Add new sprll.yml b/apiTest/PotterAPI/Add new sprll.yml index 22c8459..92f49e6 100644 --- a/apiTest/PotterAPI/Add new sprll.yml +++ b/apiTest/PotterAPI/Add new sprll.yml @@ -5,17 +5,23 @@ info: http: method: POST - url: http://localhost:3000/spells + url: http://localhost:3000/characters body: type: json - data: |- + data: | { - "spell":"Fire speed", - "effect":"fire speed forever", - "type":"Curse", - "isUnforgivable":"false" - } - auth: inherit + "name": "Lu Kankys", + "role": "student", + "house": "Gryffindor", + "school": "Hogwarts School of Witchcraft and Wizardry", + "dumbledoresArmy": true, + "bloodStatus": "half-blood", + "species": "human" + } + auth: + type: basic + username: admin + password: supersecret settings: encodeUrl: true diff --git a/bruno_test_scenario.md b/bruno_test_scenario.md new file mode 100644 index 0000000..7aaec5d --- /dev/null +++ b/bruno_test_scenario.md @@ -0,0 +1,246 @@ +# 🧪 PotterAPI – Testovací scénáře, tipy a rady pro Bruno API Client + +Tento dokument obsahuje přehled testovacích scénářů, tipů, triků a automatizovaných testů v aplikaci **Bruno** pro testování REST API **PotterAPI**. + +--- + +## ⚙️ 1. Nastavení Bruno (Environment & Kolekce) + +### Vytvoření prostředí (Environment) +V aplikaci Bruno si vytvořte nové prostředí (např. `Local`) s následujícími proměnnými: + +| Název proměnné | Hodnota | Popis | +| :--- | :--- | :--- | +| `baseUrl` | `http://localhost:3000` | Základní URL lokálního serveru | +| `username` | `****` | Přihlašovací jméno pro Basic Auth | +| `password` | `*****` | Heslo pro Basic Auth | +| `createdSpellId` | *(dynamicky)* | Ukládá ID nově vytvořeného kouzla | +| `createdCharacterId` | *(dynamicky)* | Ukládá ID nově vytvořené postavy | + +> [!TIP] +> V Bruno můžete v URL i v těle požadavku používat proměnné pomocí zápisu `{{baseUrl}}/spells`. + +--- + +## 🔑 2. Autentizace v Bruno + +Aplikace používá dva druhy autentizace: + +1. **Basic Auth (`****` / `*****`)**: + - Vyžadováno pro **`/characters`** a **`/login`**. + - **Nastavení v Bruno**: Na záložce **Auth** zvolte `Basic Auth` a zadejte `Username: {{username}}` a `Password: {{password}}`. + +2. **Content-Type Header Check (`application/json`)**: + - Vyžadováno pro `POST` a `PUT` u **`/spells`** (middleware `checkHeader`). + - **Nastavení v Bruno**: Na záložce **Headers** přidejte `Content-Type: application/json`. + +--- + +## 📋 3. Přehled testovacích scénářů + +### 🪄 A. Kouzla (`/spells`) + +#### 🟢 Pozitivní scénáře (Happy Path) +1. **Získání všech kouzel** + - **Metoda:** `GET {{baseUrl}}/spells` + - **Očekávaný stav:** `200 OK` + - **Kontrola:** Vrátí pole objektů, kde každý objekt má `id`, `spell`, `type`, `effect`, `isUnforgivable`. + +2. **Filtrování kouzel podle typu** + - **Metoda:** `GET {{baseUrl}}/spells?type=Curse` + - **Očekávaný stav:** `200 OK` + - **Kontrola:** Každá položka v poli má `"type": "Curse"`. + +3. **Vytvoření nového kouzla** + - **Metoda:** `POST {{baseUrl}}/spells` + - **Headers:** `Content-Type: application/json` + - **Body (JSON):** + ```json + { + "spell": "Expelliarmus", + "type": "Charm", + "effect": "Disarms your opponent", + "isUnforgivable": "false" + } + ``` + - **Očekávaný stav:** `201 Created` + - **Kontrola:** Vrátí `{ message: "Spell created", spell: { id: "..." } }`. Hodnota `isUnforgivable` v `spells.json` bude `false` (boolean). + +4. **Získání konkrétního kouzla podle ID** + - **Metoda:** `GET {{baseUrl}}/spells/{{createdSpellId}}` + - **Očekávaný stav:** `200 OK` + - **Kontrola:** Vrátí objekt kouzla s odpovídajícím ID. + +5. **Úprava kouzla** + - **Metoda:** `PUT {{baseUrl}}/spells/{{createdSpellId}}` + - **Headers:** `Content-Type: application/json` + - **Body (JSON):** + ```json + { + "spell": "Expelliarmus Maxima", + "type": "Charm", + "effect": "Strongly disarms opponent", + "isUnforgivable": false + } + ``` + - **Očekávaný stav:** `201 Created` + +6. **Smazání kouzla podle ID** + - **Metoda:** `DELETE {{baseUrl}}/spells/{{createdSpellId}}` + - **Očekávaný stav:** `200 OK` + - **Kontrola:** Vrátí `{ message: "spell deleted" }`. + +--- + +#### 🔴 Negativní a Hraniční scénáře +1. **Chybějící hlavička Content-Type (POST / PUT)** + - **Metoda:** `POST {{baseUrl}}/spells` (bez `Content-Type: application/json`) + - **Očekávaný stav:** `400 Bad Request` + - **Očekávaná zpráva:** `"Sorry Nick you dont have HEADer"` + +2. **Pokus o vytvoření duplicitního kouzla** + - **Metoda:** `POST {{baseUrl}}/spells` s národem kouzla, které již existuje (např. `"spell": "Alohomora"`) + - **Očekávaný stav:** `400 Bad Request` + - **Očekávaná zpráva:** `"Spell Alohomora already exists"` + +3. **Neplatná data (Joi validation failure)** + - **Metoda:** `POST {{baseUrl}}/spells` + - **Body (JSON):** (příliš krátký název, neplatný typ) + ```json + { + "spell": "Ab", + "type": "SuperPower", + "isUnforgivable": true + } + ``` + - **Očekávaný stav:** `422 Unprocessable Entity` + +4. **Vyhledání neexistujícího kouzla** + - **Metoda:** `GET {{baseUrl}}/spells/non-existing-id-999` + - **Očekávaný stav:** `404 Not Found` + +--- + +### 🧙 B. Postavy (`/characters`) + +> [!IMPORTANT] +> Všechny požadavky na `/characters` vyžadují **Basic Auth** (`****` / `*****`). + +#### 🟢 Pozitivní scénáře +1. **Získání všech postav** + - **Metoda:** `GET {{baseUrl}}/characters` + - **Auth:** Basic Auth + - **Očekávaný stav:** `200 OK` + +2. **Filtrování postav podle koleje a role** + - **Metoda:** `GET {{baseUrl}}/characters?house=Gryffindor&role=student` + - **Auth:** Basic Auth + - **Očekávaný stav:** `200 OK` + +3. **Vytvoření nové postavy** + - **Metoda:** `POST {{baseUrl}}/characters` + - **Auth:** Basic Auth + - **Headers:** `Content-Type: application/json` + - **Body (JSON):** + ```json + { + "name": "Hermione Granger", + "role": "student", + "house": "Gryffindor", + "school": "Hogwarts School of Witchcraft and Wizardry", + "dumbledoresArmy": true, + "bloodStatus": "muggle-born", + "species": "human" + } + ``` + - **Očekávaný stav:** `201 Created` + +4. **Smazání postavy podle ID** + - **Metoda:** `DELETE {{baseUrl}}/characters/{{createdCharacterId}}` + - **Auth:** Basic Auth + - **Očekávaný stav:** `200 OK` + +--- + +#### 🔴 Negativní scénáře +1. **Přístup bez přihlášení (Neplatné/Chybějící Basic Auth)** + - **Metoda:** `GET {{baseUrl}}/characters` (bez Auth) + - **Očekávaný stav:** `401 Unauthorized` + - **Očekávané tělo:** `{ "message": "Sorry Wizard, can't let you in." }` + +2. **Duplicitní jméno postavy** + - **Metoda:** `POST {{baseUrl}}/characters` se jménem, které již v databázi je (např. `"name": "Hannah Abbott"`) + - **Očekávaný stav:** `400 Bad Request` + +--- + +### 🏰 C. Koleje, Moudrý klobouk & Citáty (`/houses`, `/sortingHat`, `/quote`) + +1. **Seznam kolejí**: `GET {{baseUrl}}/houses` -> `200 OK` (Vrátí 4 koleje) +2. **Detail koleje**: `GET {{baseUrl}}/houses/5a05e2b252f039152b96b5a7` -> `200 OK` (Gryffindor) +3. **Moudrý klobouk**: `GET {{baseUrl}}/sortingHat` -> `200 OK` (Vrátí náhodnou kolej) +4. **Náhodný citát**: `GET {{baseUrl}}/quote` -> `200 OK` (Vrátí citát) + +--- + +## ⚡ 4. Testovací skripty & Asertace v Bruno + +V Bruno můžete psát testy v záložce **Tests** (používá knihovnu Chai / JavaScript). + +### Příklady asertací: + +#### Test 1: Kontrola stavového kódu a struktury odpovědi při vytvoření kouzla +*(Záložka Tests u requestu `POST /spells`)*: + +```javascript +test("Status code is 201 Created", function() { + expect(res.getStatus()).to.equal(201); +}); + +test("Response contains success message and spell ID", function() { + const data = res.getBody(); + expect(data).to.have.property("message", "Spell created"); + expect(data.spell).to.have.property("id"); +}); +``` + +#### Test 2: Uložení vytvořeného ID do proměnné prostředí (Post-response Script) +*(Záložka Script -> Post Response)*: + +```javascript +if (res.getStatus() === 201) { + const body = res.getBody(); + bru.setEnvVar("createdSpellId", body.spell.id); +} +``` + +--- + +## 🔗 5. Řetězení testů (Workflow v Bruno) + +Díky skriptování můžete vytvořit automatizovaný řetězec testů: + +```mermaid +flowchart LR + A["1. POST /spells
(Vytvořit kouzlo)"] -->|"Uloží ID do env"| B["2. GET /spells/:id
(Ověřit vytvoření)"] + B --> C["3. PUT /spells/:id
(Upravit kouzlo)"] + C --> D["4. DELETE /spells/:id
(Smazat kouzlo)"] + D --> E["5. GET /spells/:id
(Ověřit 404)"] +``` + +1. **Krok 1 (`POST /spells`)**: Vytvoří kouzlo a v `Post Response` skriptu uloží `createdSpellId`. +2. **Krok 2 (`GET /spells/{{createdSpellId}}`)**: Ověří, že kouzlo existuje. +3. **Krok 3 (`PUT /spells/{{createdSpellId}}`)**: Upraví vlastnosti kouzla. +4. **Krok 4 (`DELETE /spells/{{createdSpellId}}`)**: Smaže kouzlo. +5. **Krok 5 (`GET /spells/{{createdSpellId}}`)**: Ověří navrácení kódů `404 Not Found`. + +--- + +## 💡 Tipy a doporučení pro Bruno + +1. **Používejte Bruno Runner**: + - V levém panelu můžete kliknout na menu kolekce a zvolit **Run**. Bruno spustí všechny testy v kolekci za sebou a zobrazí zelené/červené výsledky. +2. **Ukládání kolekce do Gitu**: + - Bruno ukládá kolekce jako čisté `.bru` soubory přímo ve vaší složce projektu. Můžete je verzovat v Gitu spolu s kódem backendu! +3. **Reset databáze před testy**: + - Na začátku testovací sady doporučujeme zavolat `GET {{baseUrl}}/spells/actions/reset`, aby byl stav `spells.json` vždy předvídatelný. diff --git a/web/backend/characters.json b/web/backend/characters.json index cfa4ac7..a79ac11 100644 --- a/web/backend/characters.json +++ b/web/backend/characters.json @@ -2390,5 +2390,18 @@ "deathEater": false, "bloodStatus": "unknown", "species": "house-elf" + }, + { + "id": "51mzpz80m6x5u1zb8kogw4xn", + "name": "Lu Kankys", + "role": "student", + "house": "Gryffindor", + "school": "Hogwarts School of Witchcraft and Wizardry", + "dumbledoresArmy": true, + "bloodStatus": "half-blood", + "species": "human", + "ministryOfMagic": false, + "orderOfThePhoenix": false, + "deathEater": false } ] \ No newline at end of file diff --git a/web/backend/controllers/charactersController.js b/web/backend/controllers/charactersController.js new file mode 100644 index 0000000..5f30f2a --- /dev/null +++ b/web/backend/controllers/charactersController.js @@ -0,0 +1,145 @@ +const fs = require('fs'); +const path = require('path'); +const charactersFilePath = path.join(__dirname, '../characters.json'); + +var characters = require('../characters.json'); +const joi = require('joi'); +const initialCharacters = JSON.parse(JSON.stringify(require('../characters.json'))); +const randomId = require('random-id'); + +const schema = joi.object().keys({ + id: joi.any(), + name: joi.string().min(2).max(100).required(), + role: joi.string().allow('', null), + house: joi.string().allow('', null), + school: joi.string().allow('', null), + ministryOfMagic: joi.boolean().default(false), + orderOfThePhoenix: joi.boolean().default(false), + dumbledoresArmy: joi.boolean().default(false), + deathEater: joi.boolean().default(false), + bloodStatus: joi.string().allow('', null), + species: joi.string().allow('', null), + wand: joi.string().allow('', null), + boggart: joi.string().allow('', null), + patronus: joi.string().allow('', null), + animagus: joi.string().allow('', null), + alias: joi.string().allow('', null) +}); + +function saveCharactersToFile() { + fs.writeFileSync(charactersFilePath, JSON.stringify(characters, null, 4)); +} + +exports.characters_list = function (req, res) { + let result = characters; + if (typeof req.query.house !== 'undefined') { + result = result.filter(c => c.house === req.query.house); + } + if (typeof req.query.role !== 'undefined') { + result = result.filter(c => c.role === req.query.role); + } + if (typeof req.query.bloodStatus !== 'undefined') { + result = result.filter(c => c.bloodStatus === req.query.bloodStatus); + } + if (typeof req.query.species !== 'undefined') { + result = result.filter(c => c.species === req.query.species); + } + if (typeof req.query.limit !== 'undefined') { + result = result.slice(0, req.query.limit); + } + + return res.json(result); +}; + +exports.specific_character = function (req, res) { + let result = characters.find(c => c.id == req.params.characterId); + if (typeof result !== 'undefined') { + return res.send(result); + } + return res.status(404).send({ message: "Character not found" }); +}; + +exports.delete_all = function (req, res) { + characters = []; + saveCharactersToFile(); + return res.send({ message: 'All characters deleted' }); +}; + +exports.reset = function (req, res) { + characters = JSON.parse(JSON.stringify(initialCharacters)); + saveCharactersToFile(); + return res.send({ message: 'Characters reset', characters: characters }); +}; + +exports.delete_character = function (req, res) { + let result = characters.find(c => c.id == req.params.characterId); + if (typeof result === 'undefined') { + return res.status(404).send({ message: "Character not found" }); + } + characters = characters.filter(c => c.id != req.params.characterId); + saveCharactersToFile(); + return res.send({ message: 'Character deleted' }); +}; + +exports.update_character = function (req, res) { + let result = characters.find(c => c.id == req.params.characterId); + if (typeof result === 'undefined') { + return res.status(404).send({ message: "Character not found" }); + } + + var newChar = Object.assign({}, req.body, { id: result.id }); + const validation = schema.validate(newChar); + const { value, error } = validation; + + if (error) { + return res.status(422).json({ + message: 'Invalid character request', + data: error + }); + } + + const formattedCharacter = Object.assign({ id: value.id, name: value.name }, value); + + characters = characters.filter(c => c.id != req.params.characterId); + characters.push(formattedCharacter); + saveCharactersToFile(); + + return res.status(201).json({ + message: "Character updated", + character: { id: formattedCharacter.id } + }); +}; + +exports.new_character = function (req, res) { + var len = 24; + var pattern = '0123456789abcdef'; + + var newChar = Object.assign({}, req.body, { id: randomId(len, pattern) }); + + let existing = characters.find(c => c.name === newChar.name); + if (typeof existing !== 'undefined') { + return res.status(400).send({ + message: 'Character ' + newChar.name + ' already exists' + }); + } + + const validation = schema.validate(newChar); + const { value, error } = validation; + + if (error) { + return res.status(422).json({ + message: 'Invalid request', + data: error + }); + } + + const formattedCharacter = Object.assign({ id: value.id, name: value.name }, value); + + characters.push(formattedCharacter); + saveCharactersToFile(); + + return res.status(201).json({ + message: "Character created", + character: { id: formattedCharacter.id } + }); +}; diff --git a/web/backend/routes/characters.js b/web/backend/routes/characters.js index 3cb56e7..13a67e8 100644 --- a/web/backend/routes/characters.js +++ b/web/backend/routes/characters.js @@ -1,6 +1,6 @@ const express = require('express'); const router = express.Router(); -var characters = require('../characters.json'); +const charactersController = require('../controllers/charactersController.js'); var basicAuth = require('express-basic-auth'); router.use(basicAuth({ @@ -8,17 +8,14 @@ router.use(basicAuth({ unauthorizedResponse: { message: "Sorry Wizard, can't let you in." } +})); -})) - -router.get('/', - function (req, res) { - res.json(characters); - }); - -router.get('/:characterId', function (req, res) { - let result = characters.find(character => character.id == req.params.characterId); - res.send(result); -}); +router.get('/', charactersController.characters_list); +router.post('/', charactersController.new_character); +router.delete('/actions/deleteAll', charactersController.delete_all); +router.get('/actions/reset', charactersController.reset); +router.get('/:characterId', charactersController.specific_character); +router.delete('/:characterId', charactersController.delete_character); +router.put('/:characterId', charactersController.update_character); module.exports = router; \ No newline at end of file