Skip to content
Merged
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
26 changes: 13 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` | Ответ через две секунды для проверки таймаута |
Expand Down
4 changes: 2 additions & 2 deletions custom-server/assets/http-for-automation/login-form.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
</head>
<body>
<h1>Demo session</h1>
<form action="/http-protocol/session" method="post">
<form action="/http-for-automation/session" method="post">
<label>Username <input name="username" value="student" /></label>
<button type="submit">Sign in</button>
</form>
<p><a href="/http-protocol/session">View session</a></p>
<p><a href="/http-for-automation/session">View session</a></p>
</body>
</html>
2 changes: 0 additions & 2 deletions custom-server/assets/http-protocol/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ <h1>It works!</h1>
<li><a href="/http-protocol/stream">Stream</a></li>
<li><a href="/http-protocol/form">HTML form</a></li>
<li><a href="/http-protocol/image.png">PNG image</a></li>
<li><a href="/http-protocol/login-form">Demo session</a></li>
<li><a href="/http-protocol/cache">Cache example</a></li>
</ul>
</body>
</html>
2 changes: 1 addition & 1 deletion custom-server/src/reliability-examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface Run {
}
export default async function setUpReliabilityExamples(app: FastifyInstance) {
const runs = createDemoStore<Run>();
const prefix = '/http-protocol/reliability/runs';
const prefix = '/http-for-automation/reliability/runs';
app.post(prefix, (_request, reply) => {
const id = runs.create({
temporaryAttempts: 0,
Expand Down
20 changes: 11 additions & 9 deletions custom-server/src/session-cache-examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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',
Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion custom-server/src/webhook-examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ interface Inbox {

export default async function setUpWebhookExamples(app: FastifyInstance) {
const inboxes = createDemoStore<Inbox>();
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' });
Expand Down
48 changes: 27 additions & 21 deletions test/course-demos.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,15 @@ 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-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',
Expand All @@ -26,27 +32,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);
Expand All @@ -57,8 +63,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' });
Expand All @@ -75,14 +81,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' },
Expand All @@ -92,9 +98,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', () => {
Expand Down
Loading