refactor: Builder SoC + JWKS stub + unit tests
Builder SoC (builder/context.go):
- Moved generateDockerfile, runtimeBaseImage, zipToTarGz from handler/upload.go
to internal/builder/context.go.
Reason: knowledge about runtime images and build context structure is a
build concern, not an HTTP handler concern.
- Added PrepareContext(zipData []byte, runtime string) (*bytes.Buffer, error) —
single public entry point. Handler calls one function, gets ready buffer.
- zipToTarGz now accepts *zip.Reader instead of []byte to avoid double parsing.
- upload.go reduced from ~200 LOC to ~60 LOC (build logic gone).
auth.go — JWKS insertion point:
- Added verifySignature() stub with detailed comment explaining what v2
implementation needs (JWKS endpoint, kid lookup, RS256/ES256 verify).
- Shows exactly where to add the call in validateJWT.
Unit tests (9 total, all pass):
- controllers: TestBuildDeployment_EnvVarsSorted, TestBuildDeployment_EmptyEnv
- handler: TestHopByHopHeaders_* (3 tests)
- builder: TestPrepareContext_PythonWithRequirements, _NodeNoPackageJSON,
_UnsupportedRuntime, _DockerfileIsFirst
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
// Создано: 2026-03-11
|
||||
// Тесты для builder/context.go — PrepareContext и вспомогательных функций.
|
||||
// Работают без k8s, только со стандартной библиотекой.
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// makeTestZip создаёт zip-архив с указанными файлами.
|
||||
func makeTestZip(t *testing.T, files map[string]string) []byte {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
w := zip.NewWriter(&buf)
|
||||
for name, content := range files {
|
||||
f, err := w.Create(name)
|
||||
if err != nil {
|
||||
t.Fatalf("create zip entry %s: %v", name, err)
|
||||
}
|
||||
if _, err := f.Write([]byte(content)); err != nil {
|
||||
t.Fatalf("write zip entry %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// readTarGz разбирает tar.gz и возвращает map имя→содержимое.
|
||||
func readTarGz(t *testing.T, data *bytes.Buffer) map[string]string {
|
||||
t.Helper()
|
||||
gr, err := gzip.NewReader(data)
|
||||
if err != nil {
|
||||
t.Fatalf("gzip reader: %v", err)
|
||||
}
|
||||
defer gr.Close()
|
||||
|
||||
result := make(map[string]string)
|
||||
tr := tar.NewReader(gr)
|
||||
for {
|
||||
hdr, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("tar next: %v", err)
|
||||
}
|
||||
b, _ := io.ReadAll(tr)
|
||||
result[hdr.Name] = string(b)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// TestPrepareContext_PythonWithRequirements проверяет что для python3.11 с
|
||||
// requirements.txt генерируется правильный Dockerfile с pip install.
|
||||
func TestPrepareContext_PythonWithRequirements(t *testing.T) {
|
||||
zip := makeTestZip(t, map[string]string{
|
||||
"handler.py": "def handle(req): return 'ok'",
|
||||
"requirements.txt": "flask==3.0.0\n",
|
||||
})
|
||||
|
||||
buf, err := PrepareContext(zip, "python3.11")
|
||||
if err != nil {
|
||||
t.Fatalf("PrepareContext: %v", err)
|
||||
}
|
||||
|
||||
entries := readTarGz(t, buf)
|
||||
|
||||
// Dockerfile должен быть
|
||||
df, ok := entries["Dockerfile"]
|
||||
if !ok {
|
||||
t.Fatal("Dockerfile not found in tar")
|
||||
}
|
||||
if !strings.Contains(df, "naeel/sless-runtime-python3.11") {
|
||||
t.Errorf("Dockerfile missing python runtime image: %s", df)
|
||||
}
|
||||
if !strings.Contains(df, "pip install") {
|
||||
t.Errorf("Dockerfile missing pip install: %s", df)
|
||||
}
|
||||
|
||||
// Исходный файл должен быть в архиве
|
||||
if _, ok := entries["handler.py"]; !ok {
|
||||
t.Error("handler.py not found in tar")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPrepareContext_NodeNoPackageJSON проверяет что для nodejs20 без package.json
|
||||
// не добавляется npm install в Dockerfile.
|
||||
func TestPrepareContext_NodeNoPackageJSON(t *testing.T) {
|
||||
zip := makeTestZip(t, map[string]string{
|
||||
"index.js": "module.exports.handle = () => 'ok'",
|
||||
})
|
||||
|
||||
buf, err := PrepareContext(zip, "nodejs20")
|
||||
if err != nil {
|
||||
t.Fatalf("PrepareContext: %v", err)
|
||||
}
|
||||
|
||||
entries := readTarGz(t, buf)
|
||||
df := entries["Dockerfile"]
|
||||
|
||||
if strings.Contains(df, "npm install") {
|
||||
t.Error("npm install should NOT appear without package.json")
|
||||
}
|
||||
if !strings.Contains(df, "naeel/sless-runtime-nodejs20") {
|
||||
t.Errorf("Dockerfile missing nodejs runtime: %s", df)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPrepareContext_UnsupportedRuntime проверяет что неизвестный runtime
|
||||
// возвращает ошибку а не паникует.
|
||||
func TestPrepareContext_UnsupportedRuntime(t *testing.T) {
|
||||
zip := makeTestZip(t, map[string]string{"main.rb": "puts 'hi'"})
|
||||
|
||||
_, err := PrepareContext(zip, "ruby3.2")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unsupported runtime")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "unsupported runtime") {
|
||||
t.Errorf("unexpected error message: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPrepareContext_DockerfileIsFirst проверяет что Dockerfile — первый файл в tar.
|
||||
// Это требование kaniko: он ищет Dockerfile в корне контекста.
|
||||
func TestPrepareContext_DockerfileIsFirst(t *testing.T) {
|
||||
zip := makeTestZip(t, map[string]string{"app.py": "pass"})
|
||||
|
||||
buf, err := PrepareContext(zip, "python3.11")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
gr, _ := gzip.NewReader(buf)
|
||||
defer gr.Close()
|
||||
tr := tar.NewReader(gr)
|
||||
|
||||
hdr, err := tr.Next()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if hdr.Name != "Dockerfile" {
|
||||
t.Errorf("first tar entry should be Dockerfile, got %q", hdr.Name)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user