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
154 lines
4.2 KiB
Go
154 lines
4.2 KiB
Go
// Создано: 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)
|
||
}
|
||
}
|