App names may now contain dots, such as sneak.berlin: runs of lowercase letters and numbers joined by single dots or by hyphens, 2 to 63 characters. Docker accepts every such name in the app's image name, upaas-<name>; a dot needs a letter or number on both sides because Docker requires it. The rule also keeps a dot off either end, so the name is safe as a directory and log file name. The new and edit app forms use the same pattern. Their old one was not a valid regular expression under the flag browsers compile it with, so browsers ignored it. Model: opus-5-5
This commit was merged in pull request #267.
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
package deploy_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/docker/daemon/names"
|
||||
"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 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()
|
||||
|
||||
api := &fakeImageAPI{
|
||||
images: map[string][]string{},
|
||||
shortSHA: "abc1234",
|
||||
nextID: "sha256:built",
|
||||
}
|
||||
svc, db := newImageTestService(t, api)
|
||||
ctx := context.Background()
|
||||
|
||||
createdApp := createAppWithForm(t, db, "sneak.berlin")
|
||||
|
||||
deployment := models.NewDeployment(db)
|
||||
deployment.AppID = createdApp.ID
|
||||
require.NoError(t, deployment.Save(ctx))
|
||||
|
||||
imageID, err := svc.BuildImage(ctx, createdApp, deployment)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, svc.DeployContainer(ctx, createdApp, deployment, imageID))
|
||||
|
||||
images, _ := api.state()
|
||||
|
||||
api.mu.Lock()
|
||||
containers := slices.Clone(api.containers)
|
||||
api.mu.Unlock()
|
||||
|
||||
require.Equal(t, map[string][]string{
|
||||
"sha256:built": {"upaas-sneak.berlin:abc1234"},
|
||||
}, images)
|
||||
|
||||
_, err = reference.ParseNormalizedNamed("upaas-sneak.berlin:abc1234")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, []string{"upaas-sneak.berlin"}, containers)
|
||||
assert.Regexp(t, names.RestrictedNamePattern, containers[0])
|
||||
|
||||
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