Compare commits

...
10 Commits
10 changed files with 423 additions and 1429 deletions
+3
View File
@@ -51,3 +51,6 @@ Thumbs.db
# Docs tooling
site/
site_test/
# ВМ-деплой (не в git)
deploy-vm/
+14
View File
@@ -0,0 +1,14 @@
FROM node:18-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY server.js ./
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "server.js"]
@@ -82,3 +82,47 @@ TL;DR: docs (HTML/CSS/JS, обычно мелкие) стримятся чере
**Further Considerations**
1. Точное значение `STREAM_MAX_BYTES` и итоговый `S3_PUBLIC_BASE_URL` (path-style vs virtual-host style у Ceph RGW) — уточнить на этапе реализации/тестирования, не блокирует план.
2. Нужен ли листинг версий/лендинг на tf_docs (как у registry `/v1/providers/*/versions`) — сейчас не включено в scope, можно добавить отдельным шагом при необходимости.
---
## Решение по вариантам реализации (2026-09-02, дополнение)
Обнаружено, что `tf_docs/server.js` уже закоммичен (`4876f82 feat: add S3-backed documentation streaming service`) и использует `@aws-sdk/client-s3` (`GetObjectCommand` + `HeadBucketCommand`) — стримит весь контент через pod, без redirect. Зависимость `@aws-sdk/client-s3@3.879.0` уже в `package.json`.
Рассматривались два варианта:
1. **Redirect-версия** (по плану выше): мелкие HTML/CSS/JS стримить через pod, крупные/нестандартные файлы отдавать `302` на прямой публичный S3 URL; только stdlib Node.
2. **AWS SDK-версия** (закоммиченная): стримить весь контент через pod.
**Принято решение: оставить вариант 2, протестировать; перейти на вариант 1 только при реальных проблемах (таймауты/обрывы на крупных файлах).**
Обоснование:
- типовая документация MkDocs — мелкие HTML/CSS/JS, стриминг через pod не создаёт проблем;
- вариант 2 позволяет держать бакет `docs/*` приватным (без public policy);
- redirect нужен только при появлении крупных вложений (attachments/zip/pdf/видео).
Критерии перехода на вариант 1:
- крупный файл (pdf/zip > 2MB) через `/docs/...` обрывается или таймаутит;
- деградация при параллельных запросах;
- неприемлемая latency двойного hop (браузер → pod → S3).
Прецедент для варианта 1: инцидент `/v1/proxy` для бинарников провайдера — стриминг ZIP через pod обрывался на ~20KB, заменён на HTTP 302 redirect (см. `tf_registry/HISTORY/incident-2026-09-01-proxy-s3-dns.md`).
Redirect-версия (вариант 1) сгенерирована DeepSeek Flash, но **не записана** в `server.js`, чтобы не перезаписывать закоммиченный код. При переходе менять только `server.js`.
**Dockerfile** — до этого отсутствовал, создан и скорректирован под вариант 2:
```dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY server.js ./
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "server.js"]
```
(установка зависимостей обязательна — иначе `@aws-sdk/client-s3` не найдётся в контейнере).
+81
View File
@@ -0,0 +1,81 @@
# tf_docs: переход на 302-редирект + раздача через ВМ
Дата: 2026-09-02
## Проблема
Отдача больших MkDocs-страниц (~133 KB) через под managed-кластера `nodejsk8s`
захлёбывалась: `273 B/s` через `tf_docs` (за 80 c скачано 21 KB). Входной шлюз
managed-кластера рвёт большие тела (как в drhider: шлюз режет >~64 KB, egress не ограничен).
## Диагностика (замеры)
| Путь → S3 | Скорость |
|---|---|
| `tf_docs` (кластер) | 273 B/s |
| локальная машина (Node.js) | 24.5 KiB/s |
| **ВМ `5.172.178.213`** | **5.6 MB/s** (133 KB за 0.023 с) |
Вывод: S3 быстрый, медленный — путь от managed-кластера/внешней сети. ВМ имеет
быстрый прямой канал до S3. Паттерн решения взят из drhider («ВМ-буфер», `~/nubes/HowTo/vm-file-upload-service.md`): большие данные — мимо шлюза кластера, через ВМ.
## Решение (финальная архитектура)
```
Браузер → tf_docs (кластер, только 302) → ВМ nginx → статика (зеркало S3)
```
- `tf_docs` — тонкий редирект-роутер: `/health` + `302` на ВМ. Без S3, без стримов.
- ВМ `5.172.178.213` — nginx раздаёт статику из локального зеркала S3.
- URL без версий: `/nubes-test/...` → S3 `docs/nubes-test/nubes/...` (перезапись).
## Изменения
### `tf_docs` (репо, запушено `5360893`, версия `0.0.3`)
- `server.js` — переписан на 302-роутер (сгенерирован DeepSeek Flash):
- `GET /health` → `200 ok`
- `GET /:stand/...` → `302 Location: ${VM_DOCS_BASE_URL}/:stand/...`
- убраны `@aws-sdk/client-s3`, S3-креды, стрим
- `package.json` — `0.0.3`, зависимостей нет (чистый `http`).
- `test/server.test.js` — 4 теста (health, redirect, root, 405) — PASS.
- `.gitignore` — добавлен `deploy-vm/`.
### `tf_docs/deploy-vm/` (локально, НЕ в git)
- `nginx-tf-docs.conf` — location `/(nubes-test|nubes-dev|nubes)/` → статика `/var/www/tf-docs/`.
- `mirror-s3.sh` — `mc mirror` из S3 `docs/<stand>/nubes/` → `/var/www/tf-docs/<stand>/`.
- `README.md` — инструкция деплоя на ВМ.
### `tf_provider` (репо, запушено `dc469c6`)
- `scripts/publish-docs.sh` — заливка **без версии** (`docs/<ns>/<name>/`), перезапись `mc mirror --overwrite --remove`.
- `TOOLS/scripts/04_build_and_publish_docs.sh` — `site_url: https://<host>/<namespace>/` (без name/version); `docs_dir` относительный (для docker-сборки); хост `tf-docs.nodejsk8s.dev.nubes.ru`.
## Env tf_docs (jsonEnv в UI)
```json
{
"VM_DOCS_BASE_URL": "http://5.172.178.213"
}
```
S3-креды `tf_docs` больше не нужны (не ходит в S3). Креды — только на ВМ в `mirror-s3.sh`.
## Порядок деплоя
1. **tf_docs**: redeploy в ЛК облака (образ из gitea `5360893`), env `VM_DOCS_BASE_URL`, health `/health`.
2. **ВМ**: скопировать `deploy-vm/`, `./mirror-s3.sh nubes-test`, вставить nginx-конфиг, `nginx -t && reload`.
3. **Перезалить документацию**: `04` + `publish-docs.sh` → `docs/nubes-test/nubes/` (без версии).
## Проверка
```bash
curl -I https://tf-docs.nodejsk8s.dev.nubes.ru/nubes-test/ # 302 → ВМ
curl -I http://5.172.178.213/nubes-test/ # 200 index.html
```
## Урок (не повторять)
- `kube5s.ru` — **legacy**, закрывается. Не использовать ни в коде, ни в доках.
- HowTo-документация местами устарела (описывает legacy-домен как рабочий) — сверять с актуальными источниками.
@@ -0,0 +1,53 @@
# 2026-09-02 — Анализ Соннета по tf_docs + критическое мнение
## Вопрос (что отдавали Соннету)
Анализ финальной архитектуры отдачи MkDocs-документации через «вечный» бесплатный
домен `tf-docs.nodejsk8s.dev.nubes.ru`: браузер → под nodejsk8s (reverse-proxy) →
ВМ 5.172.178.213 (nginx/статика). Просили: правильна ли схема, можно ли ускорить
международный маршрут без платных доменов, скрытые риски.
## Ответ Соннета (суть)
1. Архитектура верна, но есть **конфигурационный дрейф**: в репо
`deploy-vm/nginx-ip-5.172.178.213.conf` всё ещё `location / { proxy_pass
http://127.0.0.1:8888/; }` (на остановленный llmui), хотя на ВМ уже `return 301`.
2. Обрывы ~16–36 КБ — похожи на инцидент tf_registry (там виноват локальный
прокси), но у нас Германия без прокси тоже режет + ВМ напрямую режет → маршрут
в РФ фильтруется на границе. Предложил **включить gzip**, чтобы «сжать страницу
ниже порога обрыва».
3. Риски: SPOF на ВМ, ВМ не выделенный хост, `location /` открыт по IP, timeout
30с (idle), TLS только на ingress.
## Моё критическое мнение
**Главное: рекомендация gzip — ошибочна.** Порог обрыва — СЕТЕВОЙ (~16.7 КБ), а не
«по объёму контента». Доказательство из замеров этой же сессии:
- файл `.test_20k.bin` (ровно 20000 байт) обрывается на 16775 байт;
- файлы 50/100/200 КБ — тоже ровно на 16773–16775;
- favicon 1017 байт — проходит полностью.
То есть обрыв наступает на ~16.7 КБ **независимо от content-length**. Сжать HTML
133 КБ gzip до <16.7 КБ нереально (текст жмётся до ~25–40 КБ) — ответ всё равно
упрётся в сетевой порог. gzip меняет размер контента, но не лимит маршрута. Гипотеза
«сжать ниже порога» не сработает. Вывод: для страниц 133–200 КБ gzip проблему не
решает.
**Остальные пункты** — валидные, но в основном уже известные/принятые trade-off'ы:
- дрейф 8888 в репо — подтверждён, реальный риск при повторном применении файла из
git (файл стоит синхронизировать с ВМ);
- SPOF на ВМ, timeout 30с, TLS-только-ingress — верно, но «ВМ-буфер» и есть
осознанное решение (весь код на ВМ);
- «location / открыт по IP» — натянуто: ВМ и так публична по IP, новой дыры нет.
**Итог:** анализ полезен, но ключевая рекомендация (gzip) технически неверна;
реально ценное — пункт про дрейф 8888 (нужно синхронизировать файл в репо).
## Статус
- Дрейф `deploy-vm/nginx-ip-5.172.178.213.conf` (8888) — ИСПРАВЛЕН: файл синхронизирован
с ВМ (`return 301 /nubes-test/` + `absolute_redirect off`). Файл gitignored, в git не
входит — риск «применения из git» не подтвердился, это локальная справочная копия.
- gzip — НЕ делать (не решает).
- Range-проброс в `server.js` — НЕ делать (для MkDocs-статики не критично).
+2 -1312
View File
File diff suppressed because it is too large Load Diff
+2 -5
View File
@@ -1,7 +1,7 @@
{
"name": "tf-docs",
"version": "0.0.0",
"description": "Terraform docs service (S3 static /docs) — initial skeleton",
"version": "0.0.5",
"description": "Terraform docs reverse-proxy (stream from VM static server)",
"main": "server.js",
"scripts": {
"start": "node server.js",
@@ -10,8 +10,5 @@
},
"engines": {
"node": ">=18"
},
"dependencies": {
"@aws-sdk/client-s3": "3.879.0"
}
}
+57
View File
@@ -0,0 +1,57 @@
Ты — инженер. Проанализируй задачу ниже и дай КРАТКИЙ, ясный ответ БЕЗ ВОДЫ: что правильно, что неправильно, что стоит поменять. Не пересказывай вводные.
---
## ЦЕЛЬ
Документация Terraform-провайдера Nubes (статический MkDocs-сайт, страницы 133–200 КБ, поисковый индекс ~1.3 МБ) должна открываться по БЕСПЛАТНОМУ «вечному» домену:
`https://tf-docs.nodejsk8s.dev.nubes.ru`
Без IP в адресной строке, без платных доменов (продление домена стоит денег — не вариант). Домен даёт облако Nubes бесплатно как managed-сервис.
## АРХИТЕКТУРА (текущая)
```
Браузер
→ tf-docs.nodejsk8s.dev.nubes.ru (managed-кластер nodejsk8s, ingress-шлюз 185.247.187.151)
→ под tf_docs (Node.js, reverse-proxy, 0.0.5)
→ ВМ 5.172.178.213 (nginx, статика из /var/www/tf-docs/)
→ MkDocs HTML
```
`tf_docs/server.js`: reverse-proxy. `/health` → 200. `/` → 301 на `/nubes-test/`. `/:stand/...` → `${VM_DOCS_BASE_URL}/:stand/...`. Форвардит content-type/content-length/cache-control/content-encoding/location. На ВМ nginx: `location /nubes-test/` отдаёт статику, `location /` = `return 301 /nubes-test/` + `absolute_redirect off`.
## ЧТО ПРОБОВАЛИ И ЧТО НЕ ПОЛУЧАЛОСЬ (хронология)
1. **Стриминг docs из S3 через под** → упиралось в ~273 Б/с (шлюз резал). Отказались.
2. **Redirect 302 с пода на ВМ** → браузер уходил на `http://5.172.178.213/...` — IP в адресе, домен терялся. Не подходит.
3. **Reverse-proxy (текущее)** → URL остаётся на домене. Принято как финальное.
4. **502 на корне `/`** → баг на ВМ: `location /` проксировал на `127.0.0.1:8888` (Docker `llmui`, остановлен по соображениям безопасности). Починено: `return 301 /nubes-test/`.
5. **301 терял заголовок `Location`** → в прокси не был в списке форвардящихся заголовков. Добавили `location`. Плюс `absolute_redirect off` на ВМ, чтобы Location был относительным (`/nubes-test/`), а не `http://5.172.178.213/...`.
## ГЛАВНАЯ ПРОБЛЕМА — СКОРОСТЬ/ОБРЫВЫ (диагностировано)
Симптом: большие ответы «замирали» ровно на ~16–20 КБ.
Замеры:
- ВМ напрямую из РФ: **5 МБ/с** (133 КБ за 0.026 с).
- Домен из РФ без прокси: **133 КБ за ~5 с, 1.3 МБ за ~5 с** — работает полностью.
- Домен через локальный v2ray-прокси (`HTTPS_PROXY=172.17.192.1:10808`): **стоп на 16788 байт**.
- Сервер в Германии (Vultr 95.179.252.111), чистый, без прокси:
- шлюз `185.247.187.151`: стоп на 16788 байт;
- ВМ `5.172.178.213` напрямую: стоп на 35912 байт (~1 КБ/с);
- контроль (google.com, не-РФ): 83 КБ за 0.48 с — канал Германии хороший.
- iotdash22.nodejsk8s.dev.nubes.ru (другой шлюз 91.214.116.216) отдаёт 17.7 КБ полностью.
Вывод, к которому пришли: режется **международный маршрут → российские IP**, а не наша архитектура. Из РФ напрямую всё быстро.
## ТЕКУЩЕЕ СОСТОЯНИЕ
Всё работает из РФ напрямую (браузер открывает страницы быстро, это подтверждено пользователем). Проверено в браузере: `/` → 301 → `/nubes-test/` (домен сохраняется), getting-started-страница (197 КБ) грузится полностью. ВМ отдаёт поду все байты (access log nginx: 133525, 200000 и т.д.).
## ЧТО ОТ ТЕБЯ НУЖНО
1. Правильна ли текущая архитектура для поставленной цели (бесплатный вечный домен + быстрая отдача из РФ)? Есть ли фундаментальные изъяны?
2. Есть ли способ ускорить/сделать надёжнее отдачу для трафика, который идёт через зарубежный маршрут (прокси/Германия), не вводя платных доменов? (или это физически нерешаемо на этом хостинге)
3. Скрытые риски/узкие места текущего решения (single point of failure на ВМ, TLS, кэш, безопасность `location /` и т.д.).
Ответ: кратко, по пунктам, без воды.
+100 -112
View File
@@ -1,136 +1,124 @@
'use strict';
const http = require('http');
const path = require('path');
const { S3Client, GetObjectCommand, HeadBucketCommand } = require('@aws-sdk/client-s3');
const https = require('https');
const PORT = process.env.PORT || 3000;
const VERSION = process.env.APP_VERSION || '0.0.0';
const SERVICE = 'tf-docs';
const S3_BUCKET = process.env.S3_BUCKET || '';
const DOCS_S3_PREFIX = (process.env.DOCS_S3_PREFIX || 'docs').replace(/^\/+|\/+$/g, '');
const S3_ENDPOINT = process.env.S3_ENDPOINT || '';
const CACHE_CONTROL = process.env.CACHE_CONTROL || 'public, max-age=60';
const PORT = Number(process.env.PORT) || 3000;
const PROXY_TIMEOUT_MS = Number(process.env.PROXY_TIMEOUT_MS) || 30000;
const VM_DOCS_BASE_URL = process.env.VM_DOCS_BASE_URL;
const s3 = new S3Client({
region: process.env.S3_REGION || 'us-east-1',
endpoint: S3_ENDPOINT ? `https://${S3_ENDPOINT.replace(/^https?:\/\//, '')}` : undefined,
forcePathStyle: true,
credentials: process.env.S3_ACCESS_KEY && process.env.S3_SECRET_KEY
? { accessKeyId: process.env.S3_ACCESS_KEY, secretAccessKey: process.env.S3_SECRET_KEY }
: undefined
});
function getDocsCandidates(urlPath) {
const relativePath = decodeURIComponent(urlPath.replace(/^\/docs\/?/, ''));
const normalized = path.posix.normalize(`/${relativePath}`).replace(/^\/+|\/+$/g, '');
if (!normalized || normalized === '.' || normalized.startsWith('../') || normalized.includes('/../')) {
return null;
}
const base = `${DOCS_S3_PREFIX}/${normalized}`;
const candidates = [];
if (urlPath.endsWith('/')) {
candidates.push(`${base}/index.html`);
} else {
candidates.push(base);
if (!path.posix.extname(normalized)) candidates.push(`${base}/index.html`);
candidates.push(`${DOCS_S3_PREFIX}/${normalized.split('/').slice(0, 3).join('/')}/index.html`);
}
return [...new Set(candidates)];
if (!VM_DOCS_BASE_URL) {
throw new Error('VM_DOCS_BASE_URL environment variable is required');
}
function contentTypeFor(key) {
const types = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.svg': 'image/svg+xml',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.webp': 'image/webp'
const UPSTREAM = new URL(VM_DOCS_BASE_URL);
if (UPSTREAM.protocol !== 'http:' && UPSTREAM.protocol !== 'https:') {
throw new Error('VM_DOCS_BASE_URL must start with http:// or https://');
}
const UPSTREAM_MODULE = UPSTREAM.protocol === 'https:' ? https : http;
const UPSTREAM_BASE_PATH = UPSTREAM.pathname.replace(/\/+$/, '');
const UPSTREAM_PORT = UPSTREAM.port || (UPSTREAM.protocol === 'https:' ? 443 : 80);
const STAND_RE = /^[A-Za-z0-9._-]+$/;
const FORWARD_HEADERS = ['content-type', 'content-length', 'cache-control', 'content-encoding', 'location'];
function respond(res, status, body) {
if (!res.headersSent) {
res.writeHead(status, {
'Content-Type': 'text/plain; charset=utf-8',
'Content-Length': Buffer.byteLength(body),
});
}
res.end(body);
}
function proxyRequest(req, res, targetPath) {
const options = {
protocol: UPSTREAM.protocol,
hostname: UPSTREAM.hostname,
port: UPSTREAM_PORT,
method: req.method,
path: targetPath,
timeout: PROXY_TIMEOUT_MS,
};
return types[path.posix.extname(key).toLowerCase()] || 'application/octet-stream';
}
async function docsHandler(req, res) {
const candidates = getDocsCandidates(req.url.split('?')[0]);
if (!candidates || !S3_BUCKET) {
res.statusCode = 400;
res.end('Invalid documentation path or S3_BUCKET is not configured');
return;
}
for (const key of candidates) {
try {
const result = await s3.send(new GetObjectCommand({ Bucket: S3_BUCKET, Key: key }));
res.statusCode = 200;
res.setHeader('Content-Type', result.ContentType || contentTypeFor(key));
res.setHeader('Cache-Control', CACHE_CONTROL);
if (result.ContentLength !== undefined) res.setHeader('Content-Length', result.ContentLength);
result.Body.pipe(res);
return;
} catch (error) {
if (error.name !== 'NoSuchKey' && error.$metadata?.httpStatusCode !== 404) {
res.statusCode = 502;
res.end('S3 request failed');
return;
const upstreamReq = UPSTREAM_MODULE.request(options, (upstreamRes) => {
const headers = {};
for (const name of FORWARD_HEADERS) {
if (upstreamRes.headers[name] !== undefined) {
headers[name] = upstreamRes.headers[name];
}
}
}
res.writeHead(upstreamRes.statusCode || 502, headers);
upstreamRes.pipe(res);
});
res.statusCode = 404;
res.end('Documentation file not found');
}
upstreamReq.on('timeout', () => {
upstreamReq.destroy(new Error('Upstream timeout'));
});
async function readyz(_req, res) {
if (!S3_BUCKET) {
res.statusCode = 200;
res.end(JSON.stringify({ status: 'ready', s3: 'not configured' }));
return;
}
try {
await s3.send(new HeadBucketCommand({ Bucket: S3_BUCKET }));
res.statusCode = 200;
res.end(JSON.stringify({ status: 'ready', s3: 'ok' }));
} catch (_error) {
res.statusCode = 503;
res.end(JSON.stringify({ status: 'not ready', s3: 'unavailable' }));
}
upstreamReq.on('error', () => {
if (res.headersSent) {
res.destroy();
} else {
respond(res, 502, 'Bad Gateway');
}
});
req.on('aborted', () => upstreamReq.destroy());
upstreamReq.end();
}
const server = http.createServer((req, res) => {
if (req.url === '/healthz') {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ status: 'ok', version: VERSION }));
if (req.method !== 'GET' && req.method !== 'HEAD') {
respond(res, 405, 'Method Not Allowed');
return;
}
if (req.url === '/readyz') {
readyz(req, res);
const rawUrl = req.url || '/';
const qIndex = rawUrl.indexOf('?');
const rawPath = qIndex === -1 ? rawUrl : rawUrl.slice(0, qIndex);
const rawQuery = qIndex === -1 ? '' : rawUrl.slice(qIndex);
if (rawPath === '/health') {
respond(res, 200, 'ok');
return;
}
if (req.url.startsWith('/docs/')) {
docsHandler(req, res);
let decoded;
try {
decoded = decodeURIComponent(rawPath);
} catch (_) {
respond(res, 400, 'Bad Request');
return;
}
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.end(`<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>${SERVICE}</title>
</head>
<body>
<h1>${SERVICE}</h1>
<p>version: ${VERSION}</p>
</body>
</html>`);
if (decoded.split('/').some((seg) => seg === '..')) {
respond(res, 400, 'Bad Request');
return;
}
let targetPath;
if (decoded === '/') {
targetPath = UPSTREAM_BASE_PATH + '/' + rawQuery;
} else {
const stand = decoded.split('/').filter(Boolean)[0];
if (!stand || !STAND_RE.test(stand)) {
respond(res, 400, 'Bad Request');
return;
}
targetPath = UPSTREAM_BASE_PATH + rawPath + rawQuery;
}
proxyRequest(req, res, targetPath);
});
server.listen(PORT, () => {
console.log(`[${SERVICE}] listening on :${PORT} (version ${VERSION})`);
});
if (require.main === module) {
server.listen(PORT, () => {
console.log(`[docs-proxy] listening on port ${PORT}, upstream: ${VM_DOCS_BASE_URL}`);
});
}
module.exports = { server };
+67
View File
@@ -0,0 +1,67 @@
'use strict';
const http = require('node:http');
const test = require('node:test');
const assert = require('node:assert');
let server;
const upstream = http.createServer((req, res) => {
res.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8',
'Content-Length': Buffer.byteLength('<html>ok</html>'),
});
res.end('<html>ok</html>');
});
test.before(async () => {
await new Promise((r) => upstream.listen(0, '127.0.0.1', r));
process.env.VM_DOCS_BASE_URL = `http://127.0.0.1:${upstream.address().port}`;
({ server } = require('../server.js'));
});
test.after(async () => {
await new Promise((r) => upstream.close(r));
});
async function withServer(fn) {
await new Promise((r) => server.listen(0, '127.0.0.1', r));
const base = `http://127.0.0.1:${server.address().port}`;
try {
await fn(base);
} finally {
await new Promise((r) => server.close(r));
}
}
test('GET /health -> 200 ok', async () => {
await withServer(async (base) => {
const res = await fetch(`${base}/health`);
assert.strictEqual(res.status, 200);
assert.strictEqual(await res.text(), 'ok');
});
});
test('GET /nubes-test/ -> proxies to upstream', async () => {
await withServer(async (base) => {
const res = await fetch(`${base}/nubes-test/`);
assert.strictEqual(res.status, 200);
assert.strictEqual(await res.text(), '<html>ok</html>');
});
});
test('GET / -> proxies to upstream root', async () => {
await withServer(async (base) => {
const res = await fetch(`${base}/`);
assert.strictEqual(res.status, 200);
assert.strictEqual(await res.text(), '<html>ok</html>');
});
});
test('POST -> 405', async () => {
await withServer(async (base) => {
const res = await fetch(`${base}/health`, { method: 'POST' });
assert.strictEqual(res.status, 405);
});
});