feat: lint-archive endpoint, UI result inline v1.3.38
This commit is contained in:
@@ -52,7 +52,7 @@ spec:
|
|||||||
serviceAccountName: fission-console
|
serviceAccountName: fission-console
|
||||||
containers:
|
containers:
|
||||||
- name: console
|
- name: console
|
||||||
image: naeel/fission-console:v1.3.36
|
image: naeel/fission-console:v1.3.38
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8090
|
- containerPort: 8090
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// maxArchiveBytes — максимальный размер zip-архива (100 KB).
|
||||||
|
// Serverless-функции не должны быть большими — только скрипты и небольшие зависимости.
|
||||||
|
maxArchiveBytes = 100 * 1024
|
||||||
|
|
||||||
|
// maxUnzippedBytes — ограничение на суммарный распакованный размер (защита от zip bomb).
|
||||||
|
// 100 KB реального кода — достаточно для любой serverless-функции.
|
||||||
|
maxUnzippedBytes = 100 * 1024
|
||||||
|
)
|
||||||
|
|
||||||
|
// lintResult — результат проверки одного файла.
|
||||||
|
type lintResult struct {
|
||||||
|
File string `json:"file"`
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
Output string `json:"output,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleLintArchive — POST /console/api/ai/lint-archive
|
||||||
|
// Принимает zip-архив (multipart/form-data, поле "archive"),
|
||||||
|
// прогоняет линтер по всем файлам с известным расширением,
|
||||||
|
// возвращает список результатов по файлам.
|
||||||
|
func (s *Server) handleLintArchive(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
writeJSONError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ограничиваем тело запроса — не более maxArchiveBytes (+ небольшой overhead для multipart)
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxArchiveBytes+4*1024)
|
||||||
|
|
||||||
|
if err := r.ParseMultipartForm(maxArchiveBytes); err != nil {
|
||||||
|
writeJSONError(w, http.StatusRequestEntityTooLarge,
|
||||||
|
fmt.Sprintf("архив слишком большой: максимум %d KB", maxArchiveBytes/1024))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f, _, err := r.FormFile("archive")
|
||||||
|
if err != nil {
|
||||||
|
writeJSONError(w, http.StatusBadRequest, "поле 'archive' обязательно")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
// Читаем архив в память
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if _, err := buf.ReadFrom(f); err != nil {
|
||||||
|
writeJSONError(w, http.StatusBadRequest, "ошибка чтения архива: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if buf.Len() > maxArchiveBytes {
|
||||||
|
writeJSONError(w, http.StatusRequestEntityTooLarge,
|
||||||
|
fmt.Sprintf("архив слишком большой: %d байт, максимум %d KB", buf.Len(), maxArchiveBytes/1024))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Открываем zip из памяти
|
||||||
|
zr, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
|
||||||
|
if err != nil {
|
||||||
|
writeJSONError(w, http.StatusBadRequest, "не удалось открыть zip: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Считаем суммарный распакованный размер (защита от zip bomb)
|
||||||
|
var totalUnzipped uint64
|
||||||
|
for _, zf := range zr.File {
|
||||||
|
totalUnzipped += zf.UncompressedSize64
|
||||||
|
}
|
||||||
|
if totalUnzipped > maxUnzippedBytes {
|
||||||
|
writeJSONError(w, http.StatusRequestEntityTooLarge,
|
||||||
|
fmt.Sprintf("суммарный размер распакованных файлов превышает %d KB", maxUnzippedBytes/1024))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Карта линтеров по расширению
|
||||||
|
type linterCfg struct {
|
||||||
|
cmd []string
|
||||||
|
stdinOk bool // поддерживает stdin
|
||||||
|
stdinArg string // аргумент для stdin-режима (имя "файла")
|
||||||
|
}
|
||||||
|
linters := map[string]linterCfg{
|
||||||
|
".py": {cmd: []string{"python3", "-m", "py_compile"}, stdinOk: false},
|
||||||
|
".js": {cmd: []string{"node", "--check"}, stdinOk: false},
|
||||||
|
".rb": {cmd: []string{"ruby", "-c"}, stdinOk: true},
|
||||||
|
".php": {cmd: []string{"php", "-l"}, stdinOk: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []lintResult
|
||||||
|
var hasError bool
|
||||||
|
|
||||||
|
for _, zf := range zr.File {
|
||||||
|
if zf.FileInfo().IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ext := strings.ToLower(filepath.Ext(zf.Name))
|
||||||
|
cfg, supported := linters[ext]
|
||||||
|
if !supported {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Читаем содержимое файла из zip в память
|
||||||
|
rc, err := zf.Open()
|
||||||
|
if err != nil {
|
||||||
|
results = append(results, lintResult{File: zf.Name, OK: false, Output: "ошибка чтения: " + err.Error()})
|
||||||
|
hasError = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var content bytes.Buffer
|
||||||
|
_, readErr := content.ReadFrom(rc)
|
||||||
|
rc.Close()
|
||||||
|
if readErr != nil {
|
||||||
|
results = append(results, lintResult{File: zf.Name, OK: false, Output: "ошибка чтения: " + readErr.Error()})
|
||||||
|
hasError = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var res lintResult
|
||||||
|
res.File = zf.Name
|
||||||
|
|
||||||
|
// Записываем во временный файл в /dev/shm (RAM) если доступен, иначе os.TempDir()
|
||||||
|
tmpDir := "/dev/shm"
|
||||||
|
if _, statErr := os.Stat(tmpDir); statErr != nil {
|
||||||
|
tmpDir = ""
|
||||||
|
}
|
||||||
|
tmpf, tmpErr := os.CreateTemp(tmpDir, "fission-lint-*"+ext)
|
||||||
|
if tmpErr != nil {
|
||||||
|
res.OK = false
|
||||||
|
res.Output = "не удалось создать tmp: " + tmpErr.Error()
|
||||||
|
results = append(results, res)
|
||||||
|
hasError = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tmpName := tmpf.Name()
|
||||||
|
_, writeErr := tmpf.Write(content.Bytes())
|
||||||
|
tmpf.Close()
|
||||||
|
|
||||||
|
if writeErr != nil {
|
||||||
|
os.Remove(tmpName)
|
||||||
|
res.OK = false
|
||||||
|
res.Output = "ошибка записи tmp: " + writeErr.Error()
|
||||||
|
results = append(results, res)
|
||||||
|
hasError = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||||
|
args := append(cfg.cmd, tmpName) //nolint:gocritic — намеренно создаём новый слайс
|
||||||
|
//nolint:gosec — cmd содержит только захардкоженные команды из linters
|
||||||
|
out, lintErr := exec.CommandContext(ctx, args[0], args[1:]...).CombinedOutput()
|
||||||
|
cancel()
|
||||||
|
os.Remove(tmpName)
|
||||||
|
|
||||||
|
outStr := strings.TrimSpace(string(out))
|
||||||
|
// Скрываем путь к tmp-файлу от пользователя
|
||||||
|
outStr = strings.ReplaceAll(outStr, tmpName, zf.Name)
|
||||||
|
|
||||||
|
if lintErr == nil {
|
||||||
|
res.OK = true
|
||||||
|
} else {
|
||||||
|
res.OK = false
|
||||||
|
hasError = true
|
||||||
|
if outStr != "" {
|
||||||
|
res.Output = outStr
|
||||||
|
} else {
|
||||||
|
res.Output = fmt.Sprintf("ошибка синтаксиса (%v)", lintErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results = append(results, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) == 0 {
|
||||||
|
writeJSONError(w, http.StatusBadRequest, "архив не содержит файлов поддерживаемых языков (.py, .js, .rb, .php)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"ok": !hasError,
|
||||||
|
"results": results,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -156,6 +156,7 @@ func (s *Server) RegisterRoutes(mux *http.ServeMux) {
|
|||||||
mux.HandleFunc("/console/api/ns/status", auth(s.handleNSStatus))
|
mux.HandleFunc("/console/api/ns/status", auth(s.handleNSStatus))
|
||||||
mux.HandleFunc("/console/api/ns/debug", auth(s.handleNSDebug))
|
mux.HandleFunc("/console/api/ns/debug", auth(s.handleNSDebug))
|
||||||
mux.HandleFunc("/console/api/ai/check", auth(s.handleAICheck))
|
mux.HandleFunc("/console/api/ai/check", auth(s.handleAICheck))
|
||||||
|
mux.HandleFunc("/console/api/ai/lint-archive", auth(s.handleLintArchive))
|
||||||
// --- ai/ask feature (удалить строку чтобы выкосить роут) ---
|
// --- ai/ask feature (удалить строку чтобы выкосить роут) ---
|
||||||
mux.HandleFunc("/console/api/ai/ask", auth(s.handleAIAsk))
|
mux.HandleFunc("/console/api/ai/ask", auth(s.handleAIAsk))
|
||||||
// --- end ai/ask feature ---
|
// --- end ai/ask feature ---
|
||||||
|
|||||||
@@ -100,7 +100,7 @@
|
|||||||
<div class="nubes">NUBES</div>
|
<div class="nubes">NUBES</div>
|
||||||
<div class="product">FISSION CONSOLE</div>
|
<div class="product">FISSION CONSOLE</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size:0.65rem; color:var(--text-secondary); margin-left:10px; align-self:center; opacity:0.7;">v1.3.36</div>
|
<div style="font-size:0.65rem; color:var(--text-secondary); margin-left:10px; align-self:center; opacity:0.7;">v1.3.38</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row" style="margin:0;">
|
<div class="row" style="margin:0;">
|
||||||
<button class="btn ghost" onclick="reloadAll()">Refresh</button>
|
<button class="btn ghost" onclick="reloadAll()">Refresh</button>
|
||||||
@@ -228,7 +228,7 @@
|
|||||||
<div id="c-archive-area" style="display:none;">
|
<div id="c-archive-area" style="display:none;">
|
||||||
<input type="file" id="c-archive-file" accept=".zip" style="display:block; margin-bottom:8px; color:var(--text-primary);">
|
<input type="file" id="c-archive-file" accept=".zip" style="display:block; margin-bottom:8px; color:var(--text-primary);">
|
||||||
<div style="display:flex; gap:6px; flex-wrap:wrap;">
|
<div style="display:flex; gap:6px; flex-wrap:wrap;">
|
||||||
<button class="btn ghost" onclick="lintArchiveFile('c')">🔍 Проверить архив линтером</button>
|
<button id="c-lint-btn" class="btn ghost" onclick="lintArchiveFile('c')">🔍 Проверка архива линтером</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="c-gen-prompt" style="display:none; margin-top:8px; display:none; gap:6px; align-items:center;">
|
<div id="c-gen-prompt" style="display:none; margin-top:8px; display:none; gap:6px; align-items:center;">
|
||||||
@@ -306,7 +306,7 @@
|
|||||||
<div id="e-archive-area" style="display:none;">
|
<div id="e-archive-area" style="display:none;">
|
||||||
<input type="file" id="e-archive-file" accept=".zip" style="display:block; margin-bottom:8px; color:var(--text-primary);">
|
<input type="file" id="e-archive-file" accept=".zip" style="display:block; margin-bottom:8px; color:var(--text-primary);">
|
||||||
<div style="display:flex; gap:6px; flex-wrap:wrap;">
|
<div style="display:flex; gap:6px; flex-wrap:wrap;">
|
||||||
<button class="btn ghost" onclick="lintArchiveFile('e')">🔍 Проверить архив линтером</button>
|
<button id="e-lint-btn" class="btn ghost" onclick="lintArchiveFile('e')">🔍 Проверка архива линтером</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="e-ai-result"
|
<div id="e-ai-result"
|
||||||
@@ -398,7 +398,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions" style="justify-content:space-between; align-items:center;">
|
<div class="actions" style="justify-content:space-between; align-items:center;">
|
||||||
<span style="font-size:0.75rem; color:var(--text-secondary);">v1.3.36</span>
|
<span style="font-size:0.75rem; color:var(--text-secondary);">v1.3.38</span>
|
||||||
<button class="btn ghost" onclick="closeHelp()">Закрыть</button>
|
<button class="btn ghost" onclick="closeHelp()">Закрыть</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,15 +17,62 @@ function setCodeMode(prefix, mode) {
|
|||||||
if (btnArchive) btnArchive.style.opacity = isCode ? '0.5' : '1';
|
if (btnArchive) btnArchive.style.opacity = isCode ? '0.5' : '1';
|
||||||
}
|
}
|
||||||
|
|
||||||
// lintArchiveFile — заглушка линтера для архивов.
|
// lintArchiveFile — отправляет zip-архив на /console/api/ai/lint-archive,
|
||||||
// Реализация (распаковка + пофайловая проверка) добавляется позже.
|
// показывает результат пофайлово в блоке {prefix}-ai-result рядом с кнопкой.
|
||||||
function lintArchiveFile(prefix) {
|
function lintArchiveFile(prefix) {
|
||||||
var input = document.getElementById(prefix + '-archive-file');
|
var input = document.getElementById(prefix + '-archive-file');
|
||||||
|
var resEl = document.getElementById(prefix + '-ai-result');
|
||||||
|
var btn = document.getElementById(prefix + '-lint-btn');
|
||||||
|
function showRes(text, bg, color) {
|
||||||
|
if (resEl) {
|
||||||
|
resEl.style.display = 'block';
|
||||||
|
resEl.style.background = bg;
|
||||||
|
resEl.style.color = color;
|
||||||
|
resEl.textContent = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!input || !input.files || !input.files[0]) {
|
if (!input || !input.files || !input.files[0]) {
|
||||||
showStatus('Выберите .zip файл', 'err');
|
showRes('Выберите .zip файл', '#3a1a1a', '#f88');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showStatus('Линтинг архива — пока не реализован (скоро)', 'info');
|
var file = input.files[0];
|
||||||
|
if (file.size > 100 * 1024) {
|
||||||
|
showRes('Архив слишком большой: максимум 100 KB', '#3a1a1a', '#f88');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (btn) { btn.disabled = true; btn.textContent = '⏳ Проверяю…'; }
|
||||||
|
showRes('Запускаю линтер…', 'var(--bg-alt)', 'var(--fg)');
|
||||||
|
var fd = new FormData();
|
||||||
|
fd.append('archive', file);
|
||||||
|
fetch(API_BASE + '/ai/lint-archive', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: authHeaders(),
|
||||||
|
body: fd
|
||||||
|
})
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
if (btn) { btn.disabled = false; btn.textContent = '🔍 Проверка архива линтером'; }
|
||||||
|
if (data.error) {
|
||||||
|
showRes('Ошибка: ' + data.error, '#3a1a1a', '#f88');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var results = data.results || [];
|
||||||
|
if (results.length === 0) {
|
||||||
|
showRes('Нет поддерживаемых файлов в архиве (.py, .js, .rb, .php)', '#3a2a00', '#ffa');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var ok = results.filter(function(r) { return r.ok; }).length;
|
||||||
|
var fail = results.filter(function(r) { return !r.ok; }).length;
|
||||||
|
var lines = results.map(function(r) {
|
||||||
|
return (r.ok ? '✅ ' : '❌ ') + r.file + (r.output ? '\n ' + r.output : '');
|
||||||
|
});
|
||||||
|
var summary = ok + ' OK, ' + fail + ' ошибок\n\n' + lines.join('\n');
|
||||||
|
showRes(summary, fail > 0 ? '#3a1a1a' : '#1a3a1a', fail > 0 ? '#f88' : '#8f8');
|
||||||
|
})
|
||||||
|
.catch(function(e) {
|
||||||
|
if (btn) { btn.disabled = false; btn.textContent = '🔍 Проверка архива линтером'; }
|
||||||
|
showRes('Ошибка сети: ' + e.message, '#3a2a00', '#ffa');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseMethods(v) {
|
function parseMethods(v) {
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# Линтинг архива функции — flow (май 2026)
|
||||||
|
|
||||||
|
## Назначение
|
||||||
|
|
||||||
|
Пользователь может загрузить zip-архив с кодом функции.
|
||||||
|
Перед сохранением или после загрузки — архив можно прогнать через линтер,
|
||||||
|
чтобы убедиться что код синтаксически корректен.
|
||||||
|
|
||||||
|
Линтинг выполняется **на сервере** (в Kubernetes), а не на машине пользователя.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Endpoint
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /console/api/ai/lint-archive
|
||||||
|
Authorization: X-Auth-Token: <sub>
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
Поле: `archive` — zip-файл.
|
||||||
|
|
||||||
|
### Ограничения
|
||||||
|
- Максимальный размер zip-архива: **100 KB**
|
||||||
|
- Максимальный суммарный размер распакованных файлов: **100 KB** (защита от zip bomb)
|
||||||
|
- При превышении — HTTP 413
|
||||||
|
|
||||||
|
### Response (200 OK)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"results": [
|
||||||
|
{"file": "hello.py", "ok": true},
|
||||||
|
{"file": "bad.js", "ok": false, "output": "SyntaxError: Unexpected token (2:0)"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response (ошибка)
|
||||||
|
```json
|
||||||
|
{"error": "архив слишком большой: максимум 100 KB"}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Поддерживаемые языки
|
||||||
|
|
||||||
|
| Расширение | Язык | Линтер |
|
||||||
|
|-----------|---------|---------------------------|
|
||||||
|
| `.py` | Python | `python3 -m py_compile` |
|
||||||
|
| `.js` | Node.js | `node --check` |
|
||||||
|
| `.rb` | Ruby | `ruby -c` |
|
||||||
|
| `.php` | PHP | `php -l` |
|
||||||
|
|
||||||
|
Файлы с другими расширениями (`.json`, `.txt`, `.md`, `.yaml` и т.д.) — пропускаются.
|
||||||
|
Если в архиве нет ни одного поддерживаемого файла — HTTP 400.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Реализация (backend)
|
||||||
|
|
||||||
|
Файл: `console/internal/api/lint_archive.go`
|
||||||
|
|
||||||
|
1. Архив читается в память (`bytes.Buffer`), без записи на диск.
|
||||||
|
2. zip открывается через `archive/zip.Reader` из буфера в памяти.
|
||||||
|
3. Проверяется суммарный распакованный размер — защита от zip bomb.
|
||||||
|
4. Каждый поддерживаемый файл читается из zip в память.
|
||||||
|
5. Записывается во временный файл в **`/dev/shm`** (RAM-backed tmpfs), fallback — `/tmp`.
|
||||||
|
6. Линтер запускается с таймаутом 10 секунд на файл.
|
||||||
|
7. Временный файл удаляется сразу после линтинга.
|
||||||
|
8. Путь к tmp-файлу скрыт от пользователя в выводе линтера.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что ещё нужно сделать
|
||||||
|
|
||||||
|
- [ ] UI: кнопка "Проверить архив" рядом с полем загрузки архива в форме создания функции
|
||||||
|
- [ ] Показывать результаты в UI (список файлов, статус, ошибки)
|
||||||
|
- [ ] Тесты: `scripts/test_fn_archives.sh` — добавить вызов lint-archive endpoint
|
||||||
Executable
+151
@@ -0,0 +1,151 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# test_lint_archive.sh — тестирует POST /console/api/ai/lint-archive
|
||||||
|
# Валидные архивы всех 4 языков → ok: true
|
||||||
|
# Архив с синтаксической ошибкой → ok: false
|
||||||
|
# Архив > 100 KB → HTTP 413
|
||||||
|
# Zip bomb (маленький zip, большой распакованный) → HTTP 413
|
||||||
|
# Архив без поддерживаемых файлов → HTTP 400
|
||||||
|
# Без поля archive → HTTP 400
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
source "${ROOT_DIR}/lib.sh"
|
||||||
|
|
||||||
|
SUB="livetest@test.local"
|
||||||
|
ARCHIVES_DIR="${ROOT_DIR}/../test-archives"
|
||||||
|
TMP=$(mktemp -d)
|
||||||
|
|
||||||
|
cleanup() { rm -rf "$TMP"; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo " test_lint_archive.sh"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
lint_post() {
|
||||||
|
local zip_path="$1"
|
||||||
|
curl -s -w "\n%{http_code}" -X POST "${BASE}/ai/lint-archive" \
|
||||||
|
-H "X-Auth-Token: ${SUB}" \
|
||||||
|
-F "archive=@${zip_path}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Вспомогательная функция: создать zip с одним файлом ─────────────────────
|
||||||
|
|
||||||
|
make_zip() {
|
||||||
|
local name="$1" filename="$2" content="$3"
|
||||||
|
local dir="${TMP}/${name}"
|
||||||
|
mkdir -p "$dir"
|
||||||
|
printf '%s' "$content" > "${dir}/${filename}"
|
||||||
|
(cd "$dir" && zip -q "../${name}.zip" "$filename")
|
||||||
|
echo "${TMP}/${name}.zip"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Python валидный ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo ">>> Python: валидный архив → ok:true"
|
||||||
|
R=$(lint_post "${ARCHIVES_DIR}/py-hello.zip")
|
||||||
|
HTTP=$(echo "$R" | tail -1)
|
||||||
|
BODY=$(echo "$R" | sed '$d')
|
||||||
|
check_http "$HTTP" "200" "Python lint → 200"
|
||||||
|
OK=$(json_field "$BODY" ok)
|
||||||
|
[ "$OK" = "True" ] || [ "$OK" = "true" ] && pass "Python lint ok=true" || fail "Python lint ok=$OK"
|
||||||
|
|
||||||
|
# ── Node.js валидный ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo ">>> Node.js: валидный архив → ok:true"
|
||||||
|
R=$(lint_post "${ARCHIVES_DIR}/node-hello.zip")
|
||||||
|
HTTP=$(echo "$R" | tail -1)
|
||||||
|
BODY=$(echo "$R" | sed '$d')
|
||||||
|
check_http "$HTTP" "200" "Node.js lint → 200"
|
||||||
|
OK=$(json_field "$BODY" ok)
|
||||||
|
[ "$OK" = "True" ] || [ "$OK" = "true" ] && pass "Node.js lint ok=true" || fail "Node.js lint ok=$OK"
|
||||||
|
|
||||||
|
# ── Ruby валидный ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo ">>> Ruby: валидный архив → ok:true"
|
||||||
|
R=$(lint_post "${ARCHIVES_DIR}/ruby-hello.zip")
|
||||||
|
HTTP=$(echo "$R" | tail -1)
|
||||||
|
BODY=$(echo "$R" | sed '$d')
|
||||||
|
check_http "$HTTP" "200" "Ruby lint → 200"
|
||||||
|
OK=$(json_field "$BODY" ok)
|
||||||
|
[ "$OK" = "True" ] || [ "$OK" = "true" ] && pass "Ruby lint ok=true" || fail "Ruby lint ok=$OK"
|
||||||
|
|
||||||
|
# ── PHP валидный ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo ">>> PHP: валидный архив → ok:true"
|
||||||
|
R=$(lint_post "${ARCHIVES_DIR}/php-hello.zip")
|
||||||
|
HTTP=$(echo "$R" | tail -1)
|
||||||
|
BODY=$(echo "$R" | sed '$d')
|
||||||
|
check_http "$HTTP" "200" "PHP lint → 200"
|
||||||
|
OK=$(json_field "$BODY" ok)
|
||||||
|
[ "$OK" = "True" ] || [ "$OK" = "true" ] && pass "PHP lint ok=true" || fail "PHP lint ok=$OK"
|
||||||
|
|
||||||
|
# ── Синтаксическая ошибка Python ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo ">>> Python: синтаксическая ошибка → ok:false"
|
||||||
|
BAD_PY=$(make_zip "bad-py" "bad.py" "def broken(:\n pass")
|
||||||
|
R=$(lint_post "$BAD_PY")
|
||||||
|
HTTP=$(echo "$R" | tail -1)
|
||||||
|
BODY=$(echo "$R" | sed '$d')
|
||||||
|
check_http "$HTTP" "200" "Python bad lint → 200"
|
||||||
|
OK=$(json_field "$BODY" ok)
|
||||||
|
[ "$OK" = "False" ] || [ "$OK" = "false" ] && pass "Python bad lint ok=false" || fail "Python bad lint ok=$OK"
|
||||||
|
|
||||||
|
# ── Синтаксическая ошибка Node.js ────────────────────────────────────────────
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo ">>> Node.js: синтаксическая ошибка → ok:false"
|
||||||
|
BAD_JS=$(make_zip "bad-js" "bad.js" "module.exports = function( { return 'broken'; };")
|
||||||
|
R=$(lint_post "$BAD_JS")
|
||||||
|
HTTP=$(echo "$R" | tail -1)
|
||||||
|
BODY=$(echo "$R" | sed '$d')
|
||||||
|
check_http "$HTTP" "200" "Node.js bad lint → 200"
|
||||||
|
OK=$(json_field "$BODY" ok)
|
||||||
|
[ "$OK" = "False" ] || [ "$OK" = "false" ] && pass "Node.js bad lint ok=false" || fail "Node.js bad lint ok=$OK"
|
||||||
|
|
||||||
|
# ── Архив > 100 KB → 413 ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo ">>> Архив > 100 KB → 413"
|
||||||
|
BIG_ZIP="${TMP}/big.zip"
|
||||||
|
# Создаём файл > 100 KB и пакуем
|
||||||
|
dd if=/dev/urandom bs=1024 count=110 2>/dev/null | base64 > "${TMP}/big.txt"
|
||||||
|
(cd "$TMP" && zip -q big.zip big.txt)
|
||||||
|
R=$(lint_post "$BIG_ZIP")
|
||||||
|
HTTP=$(echo "$R" | tail -1)
|
||||||
|
check_http "$HTTP" "413" "Большой архив → 413"
|
||||||
|
|
||||||
|
# ── Нет поддерживаемых файлов → 400 ─────────────────────────────────────────
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo ">>> Архив без поддерживаемых файлов → 400"
|
||||||
|
NO_CODE=$(make_zip "no-code" "README.md" "# Hello")
|
||||||
|
R=$(lint_post "$NO_CODE")
|
||||||
|
HTTP=$(echo "$R" | tail -1)
|
||||||
|
check_http "$HTTP" "400" "Нет поддерживаемых файлов → 400"
|
||||||
|
|
||||||
|
# ── Без поля archive → 400 ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo ">>> Без поля archive → 400"
|
||||||
|
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/ai/lint-archive" \
|
||||||
|
-H "X-Auth-Token: ${SUB}")
|
||||||
|
HTTP=$(echo "$R" | tail -1)
|
||||||
|
check_http "$HTTP" "400" "Без поля archive → 400"
|
||||||
|
|
||||||
|
# ── Невалидный zip (не zip-файл) → 400 ───────────────────────────────────────
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo ">>> Невалидный zip (текстовый файл) → 400"
|
||||||
|
echo "not a zip" > "${TMP}/fake.zip"
|
||||||
|
R=$(lint_post "${TMP}/fake.zip")
|
||||||
|
HTTP=$(echo "$R" | tail -1)
|
||||||
|
check_http "$HTTP" "400" "Невалидный zip → 400"
|
||||||
|
|
||||||
|
print_summary
|
||||||
Reference in New Issue
Block a user