Update application UI and functionality
This commit is contained in:
@@ -598,6 +598,148 @@ func TestAdminSystemAndLegacyAdminPagesServeSPA(t *testing.T) {
|
||||
if !strings.Contains(res.Body.String(), "/admin/assets/admin.js") {
|
||||
t.Fatalf("%s did not serve admin SPA shell: %s", path, res.Body.String())
|
||||
}
|
||||
if got := res.Header().Get("Cache-Control"); !strings.Contains(got, "no-store") {
|
||||
t.Fatalf("%s cache control = %q, want no-store", path, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingAdminAssetIsNotCached(t *testing.T) {
|
||||
handler, cleanup := testRouter(t)
|
||||
defer cleanup()
|
||||
req := httptest.NewRequest(http.MethodGet, "/admin/assets/missing.js", nil)
|
||||
res := httptest.NewRecorder()
|
||||
handler.ServeHTTP(res, req)
|
||||
if res.Code != http.StatusNotFound {
|
||||
t.Fatalf("missing asset returned %d: %s", res.Code, res.Body.String())
|
||||
}
|
||||
if got := res.Header().Get("Cache-Control"); got != "no-store" {
|
||||
t.Fatalf("missing asset cache control = %q, want no-store", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminReleasePackageUploadStreamsMultipart(t *testing.T) {
|
||||
handler, cleanup := testRouter(t)
|
||||
defer cleanup()
|
||||
session, csrf, err := loginForTest(handler)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req := newReleaseUploadRequest(t, "YMhut_Box_WinUI_Setup_2.0.8_x64.exe", []byte("package bytes"), true)
|
||||
req.AddCookie(&http.Cookie{Name: auth.SessionCookie, Value: session})
|
||||
req.Header.Set("X-CSRF-Token", csrf)
|
||||
res := httptest.NewRecorder()
|
||||
handler.ServeHTTP(res, req)
|
||||
if res.Code != http.StatusOK {
|
||||
t.Fatalf("upload returned %d: %s", res.Code, res.Body.String())
|
||||
}
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(res.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if payload["ok"] != true {
|
||||
t.Fatalf("unexpected upload payload: %#v", payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminReleasePackageUploadRejectsMissingAndOversizedFile(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
data []byte
|
||||
include bool
|
||||
wantStatus int
|
||||
wantCode string
|
||||
}{
|
||||
{name: "missing", include: false, wantStatus: http.StatusBadRequest, wantCode: "FILE_REQUIRED"},
|
||||
{name: "empty", include: true, data: []byte{}, wantStatus: http.StatusBadRequest, wantCode: "PACKAGE_EMPTY"},
|
||||
{name: "oversized", include: true, data: bytes.Repeat([]byte{'x'}, (1<<20)+1), wantStatus: http.StatusRequestEntityTooLarge, wantCode: "PACKAGE_TOO_LARGE"},
|
||||
{name: "unsafe-name", include: true, data: []byte("package"), wantStatus: http.StatusBadRequest, wantCode: "PACKAGE_UPLOAD_FAILED"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
handler, cleanup := testRouter(t)
|
||||
defer cleanup()
|
||||
session, csrf, err := loginForTest(handler)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fileName := "package.exe"
|
||||
if tc.name == "unsafe-name" {
|
||||
fileName = "../package.exe"
|
||||
}
|
||||
req := newReleaseUploadRequest(t, fileName, tc.data, tc.include)
|
||||
req.AddCookie(&http.Cookie{Name: auth.SessionCookie, Value: session})
|
||||
req.Header.Set("X-CSRF-Token", csrf)
|
||||
res := httptest.NewRecorder()
|
||||
handler.ServeHTTP(res, req)
|
||||
if res.Code != tc.wantStatus || !strings.Contains(res.Body.String(), tc.wantCode) {
|
||||
t.Fatalf("upload returned %d %s, want %d containing %s", res.Code, res.Body.String(), tc.wantStatus, tc.wantCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newReleaseUploadRequest(t *testing.T, name string, data []byte, includeFile bool) *http.Request {
|
||||
t.Helper()
|
||||
var body bytes.Buffer
|
||||
writer := multipart.NewWriter(&body)
|
||||
if includeFile {
|
||||
part, err := writer.CreateFormFile("file", name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := part.Write(data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := writer.WriteField("version", "2.0.8"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(name, "..") {
|
||||
if err := writer.WriteField("fileName", name); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/admin/releases/packages", &body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
return req
|
||||
}
|
||||
|
||||
func TestValidateAdminDiskBuildRejectsMissingReferencedAsset(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte(`<script type="module" src="/admin/assets/missing.js"></script>`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := validateAdminDiskBuild(dir); err == nil || !strings.Contains(err.Error(), "assets/missing.js") {
|
||||
t.Fatalf("validateAdminDiskBuild returned %v, want missing asset error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncompleteAdminDiskBuildDoesNotServeDiskAssets(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(dir, "assets"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte(`<script type="module" src="/admin/assets/missing.js"></script>`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "assets", "stale.js"), []byte(`stale disk asset`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := &router{cfg: &config.Config{AdminWebDir: dir}}
|
||||
req := httptest.NewRequest(http.MethodGet, "/admin/assets/stale.js", nil)
|
||||
res := httptest.NewRecorder()
|
||||
r.serveAdminAsset(res, req, "assets/stale.js")
|
||||
if res.Code != http.StatusNotFound {
|
||||
t.Fatalf("incomplete disk asset returned %d: %s", res.Code, res.Body.String())
|
||||
}
|
||||
if strings.Contains(res.Body.String(), "stale disk asset") {
|
||||
t.Fatal("incomplete disk build served a stale disk asset")
|
||||
}
|
||||
if got := res.Header().Get("Cache-Control"); got != "no-store" {
|
||||
t.Fatalf("incomplete asset cache control = %q, want no-store", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -796,6 +938,26 @@ func loginForTest(handler http.Handler) (string, string, error) {
|
||||
return "", "", errors.New("session cookie not set")
|
||||
}
|
||||
|
||||
func TestAdminLoginFailureReturnsImmediatelyAndIsNotCached(t *testing.T) {
|
||||
handler, cleanup := testRouter(t)
|
||||
defer cleanup()
|
||||
body := bytes.NewBufferString(`{"username":"admin","password":"wrong","captchaId":"missing","captcha":"00000"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/admin/auth/login", body)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
res := httptest.NewRecorder()
|
||||
started := time.Now()
|
||||
handler.ServeHTTP(res, req)
|
||||
if time.Since(started) > time.Second {
|
||||
t.Fatal("invalid login request did not return promptly")
|
||||
}
|
||||
if res.Code != http.StatusOK || !strings.Contains(res.Body.String(), "LOGIN_FAILED") {
|
||||
t.Fatalf("invalid login returned %d: %s", res.Code, res.Body.String())
|
||||
}
|
||||
if got := res.Header().Get("Cache-Control"); got != "no-store" {
|
||||
t.Fatalf("login cache control = %q, want no-store", got)
|
||||
}
|
||||
}
|
||||
|
||||
func readTestCaptcha(dataURL string) (string, error) {
|
||||
const prefix = "data:image/png;base64,"
|
||||
raw, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(dataURL, prefix))
|
||||
@@ -936,6 +1098,7 @@ func testRouter(t *testing.T) (http.Handler, func()) {
|
||||
TimestampWindowSeconds: 600,
|
||||
MaxRequestBytes: 12 << 20,
|
||||
MaxPackageBytes: 10 << 20,
|
||||
ReleaseUploadMaxBytes: 1 << 20,
|
||||
Database: config.DatabaseConfig{
|
||||
Provider: "sqlite",
|
||||
SQLitePath: filepath.Join(root, "storage", "unified.sqlite"),
|
||||
|
||||
Reference in New Issue
Block a user