Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e3c73de09 | ||
|
|
5c836c085d |
@@ -1,6 +1,9 @@
|
||||
# .git is sent so that `make build` in the Dockerfile can stamp the commit into
|
||||
# upaas. List no tracked file here: git would see it as deleted in the build and
|
||||
# the version would end in -dirty.
|
||||
# .git is sent without its config, because a remote URL there can carry a
|
||||
# credential; `git describe` does not need it.
|
||||
.git/config
|
||||
.env
|
||||
bin/
|
||||
.vscode/
|
||||
|
||||
@@ -27,6 +27,10 @@ regress.
|
||||
app forms check the same rule; browsers ignored their old pattern, which was
|
||||
not a valid regular expression there (#260).
|
||||
|
||||
- 2026-10-02: `.dockerignore` leaves out `.git/config`, so a remote URL there
|
||||
that carries a credential no longer goes into the Docker build; the image
|
||||
still shows the commit it was built from (#269).
|
||||
|
||||
- 2026-10-02: The build no longer passes the CPU architecture in: upaas reads it
|
||||
from Go's `runtime.GOARCH` when it runs, and the startup log line reports it
|
||||
as `arch`. `CONVENTIONS.md` follows the updated conventions in `sneak/prompts`
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
package handlers //nolint:testpackage // testing unexported validateAppName
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"sneak.berlin/go/upaas/internal/models"
|
||||
"sneak.berlin/go/upaas/templates"
|
||||
)
|
||||
|
||||
func TestValidateAppName(t *testing.T) {
|
||||
@@ -63,3 +72,42 @@ func TestValidateAppName(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAppNameErrorMentionsDots(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := validateAppName("a..b")
|
||||
|
||||
require.ErrorIs(t, err, errAppNamePattern)
|
||||
assert.Contains(t, err.Error(), "dots")
|
||||
}
|
||||
|
||||
// TestAppFormsCheckAppNameLikeServer checks that the name field on the new
|
||||
// and edit app forms has the server's pattern and length limits, and that
|
||||
// its hint mentions dots.
|
||||
func TestAppFormsCheckAppNameLikeServer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
pattern := strings.TrimSuffix(strings.TrimPrefix(validAppNameRe.String(), "^"), "$")
|
||||
|
||||
pages := map[string]map[string]any{
|
||||
"app_new.html": {},
|
||||
"app_edit.html": {dataKeyApp: &models.App{}},
|
||||
}
|
||||
|
||||
for page, data := range pages {
|
||||
var out bytes.Buffer
|
||||
|
||||
require.NoError(t, templates.GetParsed().ExecuteTemplate(&out, page, data))
|
||||
|
||||
// The name field and its hint, up to the end of their div.
|
||||
_, field, found := strings.Cut(out.String(), `id="name"`)
|
||||
require.True(t, found, page)
|
||||
|
||||
field, _, _ = strings.Cut(field, "</div>")
|
||||
assert.Contains(t, field, `pattern="`+pattern+`"`, page)
|
||||
assert.Contains(t, field, `minlength="`+strconv.Itoa(appNameMinLength)+`"`, page)
|
||||
assert.Contains(t, field, `maxlength="`+strconv.Itoa(appNameMaxLength)+`"`, page)
|
||||
assert.Contains(t, field, "hyphens, and dots", page)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,13 @@ package deploy_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
@@ -10,11 +16,17 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"sneak.berlin/go/upaas/internal/database"
|
||||
"sneak.berlin/go/upaas/internal/globals"
|
||||
"sneak.berlin/go/upaas/internal/handlers"
|
||||
"sneak.berlin/go/upaas/internal/logger"
|
||||
"sneak.berlin/go/upaas/internal/models"
|
||||
"sneak.berlin/go/upaas/internal/service/app"
|
||||
)
|
||||
|
||||
// TestDeployAppWithDotInName deploys an app named sneak.berlin against a
|
||||
// fake Docker API and checks that Docker accepts the names of the image it
|
||||
// TestDeployAppWithDotInName creates an app named sneak.berlin through the
|
||||
// new app form, as a user does, then builds and deploys it against a fake
|
||||
// Docker API and checks that Docker accepts the names of the image it
|
||||
// builds and of the container it runs, using Docker's own rules for each.
|
||||
func TestDeployAppWithDotInName(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -27,15 +39,15 @@ func TestDeployAppWithDotInName(t *testing.T) {
|
||||
svc, db := newImageTestService(t, api)
|
||||
ctx := context.Background()
|
||||
|
||||
app := saveApp(t, db, "sneak.berlin", "", "")
|
||||
createdApp := createAppWithForm(t, db, "sneak.berlin")
|
||||
|
||||
deployment := models.NewDeployment(db)
|
||||
deployment.AppID = app.ID
|
||||
deployment.AppID = createdApp.ID
|
||||
require.NoError(t, deployment.Save(ctx))
|
||||
|
||||
imageID, err := svc.BuildImage(ctx, app, deployment)
|
||||
imageID, err := svc.BuildImage(ctx, createdApp, deployment)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, svc.DeployContainer(ctx, app, deployment, imageID))
|
||||
require.NoError(t, svc.DeployContainer(ctx, createdApp, deployment, imageID))
|
||||
|
||||
images, _ := api.state()
|
||||
|
||||
@@ -53,5 +65,56 @@ func TestDeployAppWithDotInName(t *testing.T) {
|
||||
require.Equal(t, []string{"upaas-sneak.berlin"}, containers)
|
||||
assert.Regexp(t, names.RestrictedNamePattern, containers[0])
|
||||
|
||||
assert.DirExists(t, svc.GetBuildDirExported(app.Name))
|
||||
assert.DirExists(t, svc.GetBuildDirExported(createdApp.Name))
|
||||
}
|
||||
|
||||
// createAppWithForm posts the new app form with the given name, which
|
||||
// checks the name as it does for a user, and returns the app it created.
|
||||
func createAppWithForm(
|
||||
t *testing.T,
|
||||
db *database.Database,
|
||||
name string,
|
||||
) *models.App {
|
||||
t.Helper()
|
||||
|
||||
log := logger.NewForTest(slog.New(slog.NewTextHandler(os.Stderr, nil)))
|
||||
|
||||
appSvc, err := app.New(nil, app.ServiceParams{Logger: log, Database: db})
|
||||
require.NoError(t, err)
|
||||
|
||||
globalInstance, err := globals.New(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
handlersInstance, err := handlers.New(nil, handlers.Params{
|
||||
Logger: log,
|
||||
Globals: globalInstance,
|
||||
Database: db,
|
||||
App: appSvc,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
form := url.Values{
|
||||
"name": {name},
|
||||
"repo_url": {"git@example.com:sneak/" + name + ".git"},
|
||||
}
|
||||
request := httptest.NewRequestWithContext(
|
||||
t.Context(), http.MethodPost, "/apps", strings.NewReader(form.Encode()),
|
||||
)
|
||||
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
handlersInstance.HandleAppCreate().ServeHTTP(recorder, request)
|
||||
|
||||
// The form redirects to the new app's page; it shows the form again,
|
||||
// with the error, when it refuses the name.
|
||||
require.Equal(t, http.StatusSeeOther, recorder.Code, recorder.Body.String())
|
||||
|
||||
appID, found := strings.CutPrefix(recorder.Header().Get("Location"), "/apps/")
|
||||
require.True(t, found)
|
||||
|
||||
createdApp, err := models.FindApp(t.Context(), db, appID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, createdApp)
|
||||
|
||||
return createdApp
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user