From 563d109ae2ed1545b1d123d8162eacd71f244217 Mon Sep 17 00:00:00 2001 From: Nikolay Gagarinov Date: Thu, 22 Oct 2026 22:10:27 +0500 Subject: [PATCH 1/2] =?UTF-8?q?fix(http):=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BD?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=B8=20=D0=BE=D0=BF=D1=8B=D1=82=D1=8B=20?= =?UTF-8?q?=D0=BA=D1=83=D1=80=D1=81=D0=B0=20HTTP=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=B0=D0=B2=D1=82=D0=BE=D0=BC=D0=B0=D1=82=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20=D0=BF=D0=BE=D0=B4=20=D0=B5=D0=B3=D0=BE=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B5=D1=84=D0=B8=D0=BA=D1=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Сессия, кеш, вебхуки и повторы жили под /http-protocol, хотя эндпоинты группируются по имени курса. Теперь они под /http-for-automation, путь куки тоже. Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 26 ++++++------ .../http-for-automation/login-form.html | 4 +- custom-server/assets/http-protocol/index.html | 2 - custom-server/src/reliability-examples.ts | 2 +- custom-server/src/session-cache-examples.ts | 20 +++++---- custom-server/src/webhook-examples.ts | 2 +- test/course-demos.test.ts | 42 +++++++++---------- 7 files changed, 49 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 5c5e17a..432398e 100644 --- a/README.md +++ b/README.md @@ -54,22 +54,22 @@ ## Демонстрации HTTP для автоматизации -Курс использует отдельные обработчики сессии, кеширования, событий и повторных запросов. Общие коллекции задач, пользователей и публикаций сохраняются. +Обработчики сессии, кеширования, событий и повторных запросов живут под префиксом курса `/http-for-automation`. Общие коллекции задач, пользователей и публикаций сохраняются. | Метод и путь | Результат | | --- | --- | -| `GET /http-protocol/login-form` | Форма с учебным именем `student` | -| `POST /http-protocol/session` | Куки `session=hexlet-demo` на пять минут | -| `GET /http-protocol/session` | `Guest` без куки, `Welcome, student!` с учебной куки | -| `DELETE /http-protocol/session` | Удаление куки клиента | -| `GET /http-protocol/cache` | Расписание, `ETag` и `private, max-age=60` | -| `GET /http-protocol/cache/revalidate` | То же расписание с `no-cache` | -| `GET /http-protocol/cache/no-store` | То же расписание с `no-store` | -| `POST /http-protocol/webhooks/inboxes` | Новый приёмник, его `callback_url` и `events_url` | -| `POST /http-protocol/webhooks/inboxes/{id}/subscription` | Подписка с полученным `callback_url` | -| `POST /http-protocol/webhooks/inboxes/{id}/trigger` | Отправка учебного события настоящим HTTP-запросом в свой приёмник | -| `GET /http-protocol/webhooks/inboxes/{id}/events` | Сохранённые входящие сообщения и ответы приёмника | -| `POST /http-protocol/reliability/runs` | Новый отдельный опыт с `run_id` и `base_url` | +| `GET /http-for-automation/login-form` | Форма с учебным именем `student` | +| `POST /http-for-automation/session` | Куки `session=hexlet-demo` на пять минут | +| `GET /http-for-automation/session` | `Guest` без куки, `Welcome, student!` с учебной куки | +| `DELETE /http-for-automation/session` | Удаление куки клиента | +| `GET /http-for-automation/cache` | Расписание, `ETag` и `private, max-age=60` | +| `GET /http-for-automation/cache/revalidate` | То же расписание с `no-cache` | +| `GET /http-for-automation/cache/no-store` | То же расписание с `no-store` | +| `POST /http-for-automation/webhooks/inboxes` | Новый приёмник, его `callback_url` и `events_url` | +| `POST /http-for-automation/webhooks/inboxes/{id}/subscription` | Подписка с полученным `callback_url` | +| `POST /http-for-automation/webhooks/inboxes/{id}/trigger` | Отправка учебного события настоящим HTTP-запросом в свой приёмник | +| `GET /http-for-automation/webhooks/inboxes/{id}/events` | Сохранённые входящие сообщения и ответы приёмника | +| `POST /http-for-automation/reliability/runs` | Новый отдельный опыт с `run_id` и `base_url` | | `GET {base_url}/temporary` | Первый ответ 503 с `Retry-After: 1`, следующий 200 | | `GET {base_url}/limited` | 429 с `Retry-After: 1`, после ожидания 200 | | `GET {base_url}/slow` | Ответ через две секунды для проверки таймаута | diff --git a/custom-server/assets/http-for-automation/login-form.html b/custom-server/assets/http-for-automation/login-form.html index d40bc8a..f5cf277 100644 --- a/custom-server/assets/http-for-automation/login-form.html +++ b/custom-server/assets/http-for-automation/login-form.html @@ -7,10 +7,10 @@

Demo session

-
+
-

View session

+

View session

diff --git a/custom-server/assets/http-protocol/index.html b/custom-server/assets/http-protocol/index.html index 95a0377..59cd05c 100644 --- a/custom-server/assets/http-protocol/index.html +++ b/custom-server/assets/http-protocol/index.html @@ -13,8 +13,6 @@

It works!

  • Stream
  • HTML form
  • PNG image
  • -
  • Demo session
  • -
  • Cache example
  • diff --git a/custom-server/src/reliability-examples.ts b/custom-server/src/reliability-examples.ts index e6f5547..2a9bfcd 100644 --- a/custom-server/src/reliability-examples.ts +++ b/custom-server/src/reliability-examples.ts @@ -17,7 +17,7 @@ interface Run { } export default async function setUpReliabilityExamples(app: FastifyInstance) { const runs = createDemoStore(); - const prefix = '/http-protocol/reliability/runs'; + const prefix = '/http-for-automation/reliability/runs'; app.post(prefix, (_request, reply) => { const id = runs.create({ temporaryAttempts: 0, diff --git a/custom-server/src/session-cache-examples.ts b/custom-server/src/session-cache-examples.ts index 2e0dd16..de98718 100644 --- a/custom-server/src/session-cache-examples.ts +++ b/custom-server/src/session-cache-examples.ts @@ -4,18 +4,20 @@ const OFFICE_HOURS = 'Office hours: 09:00-18:00'; const VERSION = '"office-v1"'; export default async function setUpSessionCacheExamples(app: FastifyInstance) { - app.get('/http-protocol/login-form', (_request, reply) => reply.sendFile('http-protocol/login-form.html')); - app.get('/http-protocol/session', (request, reply) => { + app.get('/http-for-automation/login-form', (_request, reply) => + reply.sendFile('http-for-automation/login-form.html'), + ); + app.get('/http-for-automation/session', (request, reply) => { const text = request.cookies.session === 'hexlet-demo' ? 'Welcome, student!' : 'Guest'; return reply.type('text/plain; charset=utf-8').header('Cache-Control', 'no-store').send(text); }); - app.post<{ Body: { username?: string } }>('/http-protocol/session', (request, reply) => { + app.post<{ Body: { username?: string } }>('/http-for-automation/session', (request, reply) => { if (request.body?.username !== 'student') { return reply.code(400).type('text/plain; charset=utf-8').send('Use the demo username student'); } return reply .setCookie('session', 'hexlet-demo', { - path: '/http-protocol', + path: '/http-for-automation', maxAge: 300, httpOnly: true, secure: true, @@ -25,10 +27,10 @@ export default async function setUpSessionCacheExamples(app: FastifyInstance) { .header('Cache-Control', 'no-store') .send('Session started'); }); - app.delete('/http-protocol/session', (_request, reply) => + app.delete('/http-for-automation/session', (_request, reply) => reply .clearCookie('session', { - path: '/http-protocol', + path: '/http-for-automation', httpOnly: true, secure: true, sameSite: 'lax', @@ -39,9 +41,9 @@ export default async function setUpSessionCacheExamples(app: FastifyInstance) { ); for (const [url, policy] of [ - ['/http-protocol/cache', 'private, max-age=60'], - ['/http-protocol/cache/revalidate', 'no-cache'], - ['/http-protocol/cache/no-store', 'no-store'], + ['/http-for-automation/cache', 'private, max-age=60'], + ['/http-for-automation/cache/revalidate', 'no-cache'], + ['/http-for-automation/cache/no-store', 'no-store'], ]) { app.get(url, (request, reply) => { reply.header('Cache-Control', policy); diff --git a/custom-server/src/webhook-examples.ts b/custom-server/src/webhook-examples.ts index 36e256c..51884b6 100644 --- a/custom-server/src/webhook-examples.ts +++ b/custom-server/src/webhook-examples.ts @@ -16,7 +16,7 @@ interface Inbox { export default async function setUpWebhookExamples(app: FastifyInstance) { const inboxes = createDemoStore(); - const prefix = '/http-protocol/webhooks/inboxes'; + const prefix = '/http-for-automation/webhooks/inboxes'; app.post(prefix, (_request, reply) => { const id = inboxes.create({ subscribed: false, delivering: false, nextEvent: 1, events: [] }); if (!id) return reply.code(503).send({ error: 'demo_capacity_reached' }); diff --git a/test/course-demos.test.ts b/test/course-demos.test.ts index 8c726b8..051919a 100644 --- a/test/course-demos.test.ts +++ b/test/course-demos.test.ts @@ -15,8 +15,8 @@ afterAll(() => app.close()); describe('Куки и условные запросы', () => { it('вход выдаёт куки, которая меняет ответ страницы сессии', async () => { - expect((await client.send('/http-protocol/session')).body).toBe('Guest'); - const login = await client.send('/http-protocol/session', { + expect((await client.send('/http-for-automation/session')).body).toBe('Guest'); + const login = await client.send('/http-for-automation/session', { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, payload: 'username=student', @@ -26,27 +26,27 @@ describe('Куки и условные запросы', () => { expect(login.headers['set-cookie']).toContain('session=hexlet-demo'); expect(login.headers['set-cookie']).toContain('Max-Age=300'); expect(login.headers['set-cookie']).toContain('HttpOnly; Secure; SameSite=Lax'); - const member = await client.send('/http-protocol/session', { headers: { cookie: 'session=hexlet-demo' } }); + const member = await client.send('/http-for-automation/session', { headers: { cookie: 'session=hexlet-demo' } }); expect(member.body).toBe('Welcome, student!'); expect(member.headers['cache-control']).toBe('no-store'); - expect((await client.send('/http-protocol/session', { method: 'DELETE' })).headers['set-cookie']).toContain( + expect((await client.send('/http-for-automation/session', { method: 'DELETE' })).headers['set-cookie']).toContain( 'Max-Age=0', ); }); it('подтверждает версию без тела и отличает no-cache от no-store', async () => { - const first = await client.send('/http-protocol/cache'); + const first = await client.send('/http-for-automation/cache'); expect(first.body).toBe('Office hours: 09:00-18:00'); expect(first.headers['content-length']).toBe('25'); expect(first.headers['cache-control']).toBe('private, max-age=60'); - const match = await client.send('/http-protocol/cache', { headers: { 'if-none-match': first.headers.etag } }); + const match = await client.send('/http-for-automation/cache', { headers: { 'if-none-match': first.headers.etag } }); expect(match.status).toBe(304); expect(match.body).toBe(''); expect(match.headers['content-length']).toBeUndefined(); - expect((await client.send('/http-protocol/cache', { headers: { 'if-none-match': '"old-version"' } })).status).toBe( - 200, - ); - expect((await client.send('/http-protocol/cache/revalidate')).headers['cache-control']).toBe('no-cache'); - const noStore = await client.send('/http-protocol/cache/no-store', { + expect( + (await client.send('/http-for-automation/cache', { headers: { 'if-none-match': '"old-version"' } })).status, + ).toBe(200); + expect((await client.send('/http-for-automation/cache/revalidate')).headers['cache-control']).toBe('no-cache'); + const noStore = await client.send('/http-for-automation/cache/no-store', { headers: { 'if-none-match': first.headers.etag }, }); expect(noStore.status).toBe(200); @@ -57,8 +57,8 @@ describe('Куки и условные запросы', () => { describe('Доставка вебхука', () => { it('требует подписку и делает настоящий HTTP-запрос в отдельный приёмник', async () => { - const created = await client.json('/http-protocol/webhooks/inboxes', 'POST'); - const prefix = `/http-protocol/webhooks/inboxes/${created.body.inbox_id}`; + const created = await client.json('/http-for-automation/webhooks/inboxes', 'POST'); + const prefix = `/http-for-automation/webhooks/inboxes/${created.body.inbox_id}`; expect(created.status).toBe(201); expect((await client.json(`${prefix}/trigger`, 'POST')).status).toBe(409); const unsafe = await client.json(`${prefix}/subscription`, 'POST', { callback_url: 'http://127.0.0.1/admin' }); @@ -75,14 +75,14 @@ describe('Доставка вебхука', () => { response_status: 204, body: { event_id: 'evt-1', type: 'task.created', data: { id: 101, title: 'Check the webhook' } }, }); - const other = await client.json('/http-protocol/webhooks/inboxes', 'POST'); - expect((await client.send(`/http-protocol/webhooks/inboxes/${other.body.inbox_id}/events`)).body.events).toEqual( - [], - ); + const other = await client.json('/http-for-automation/webhooks/inboxes', 'POST'); + expect( + (await client.send(`/http-for-automation/webhooks/inboxes/${other.body.inbox_id}/events`)).body.events, + ).toEqual([]); }); it('отклоняет сообщение без учебного подтверждения источника', async () => { - const inbox = await client.json('/http-protocol/webhooks/inboxes', 'POST'); - const denied = await client.json(`/http-protocol/webhooks/inboxes/${inbox.body.inbox_id}`, 'POST', { + const inbox = await client.json('/http-for-automation/webhooks/inboxes', 'POST'); + const denied = await client.json(`/http-for-automation/webhooks/inboxes/${inbox.body.inbox_id}`, 'POST', { event_id: 'evt-1', type: 'task.created', data: { id: 101, title: 'Check the webhook' }, @@ -92,9 +92,9 @@ describe('Доставка вебхука', () => { }); const createRun = async () => { - const reply = await client.json('/http-protocol/reliability/runs', 'POST'); + const reply = await client.json('/http-for-automation/reliability/runs', 'POST'); expect(reply.status).toBe(201); - return `/http-protocol/reliability/runs/${reply.body.run_id}`; + return `/http-for-automation/reliability/runs/${reply.body.run_id}`; }; describe('Управляемые сбои HTTP', () => { From 2a8e97429e3c80930dfe975bc8396413cef16429 Mon Sep 17 00:00:00 2001 From: Nikolay Gagarinov Date: Thu, 22 Oct 2026 22:13:36 +0500 Subject: [PATCH 2/2] =?UTF-8?q?test(http):=20=D0=BF=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=B8=D1=82=D1=8C=20=D0=BE=D1=82=D0=B4=D0=B0=D1=87?= =?UTF-8?q?=D1=83=20=D1=84=D0=BE=D1=80=D0=BC=D1=8B=20=D0=B2=D1=85=D0=BE?= =?UTF-8?q?=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) --- test/course-demos.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/course-demos.test.ts b/test/course-demos.test.ts index 051919a..9e5486e 100644 --- a/test/course-demos.test.ts +++ b/test/course-demos.test.ts @@ -14,6 +14,12 @@ beforeAll(async () => { afterAll(() => app.close()); describe('Куки и условные запросы', () => { + it('форма входа отправляет имя на страницу сессии', async () => { + const page = await client.send('/http-for-automation/login-form'); + expect(page.status).toBe(200); + expect(page.body).toContain('action="/http-for-automation/session"'); + }); + it('вход выдаёт куки, которая меняет ответ страницы сессии', async () => { expect((await client.send('/http-for-automation/session')).body).toBe('Guest'); const login = await client.send('/http-for-automation/session', {