init
This commit is contained in:
commit
b15b95781c
108 changed files with 14802 additions and 0 deletions
323
internal/http/projects_integration_test.go
Normal file
323
internal/http/projects_integration_test.go
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
package httpserver_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAdminProjectTagAndReleaseFlow(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
router, cfg, store := newTestRouterWithStore(t)
|
||||
|
||||
sessionCookie := loginAsAdmin(t, router)
|
||||
|
||||
projectLocation := submitForm(t, router, http.MethodPost, "/admin/projects", url.Values{
|
||||
"name": {"Desktop App"},
|
||||
"slug": {""},
|
||||
"description": {"Primary desktop updater stream."},
|
||||
}, http.StatusSeeOther, sessionCookie)
|
||||
projectID := extractResourceID(t, projectLocation, "/admin/projects/")
|
||||
|
||||
submitForm(t, router, http.MethodPost, "/admin/projects/"+projectID, url.Values{
|
||||
"name": {"Desktop App"},
|
||||
"slug": {"desktop-app"},
|
||||
"description": {"Primary desktop updater stream with release uploads."},
|
||||
}, http.StatusSeeOther, sessionCookie)
|
||||
|
||||
tagLocation := submitForm(t, router, http.MethodPost, "/admin/tags", url.Values{
|
||||
"name": {"Windows Stable"},
|
||||
"slug": {""},
|
||||
"description": {"Windows production releases."},
|
||||
}, http.StatusSeeOther, sessionCookie)
|
||||
tagID := extractResourceID(t, tagLocation, "/admin/tags/")
|
||||
|
||||
submitForm(t, router, http.MethodPost, "/admin/projects/"+projectID+"/tags", url.Values{
|
||||
"tag_id": {tagID},
|
||||
}, http.StatusSeeOther, sessionCookie)
|
||||
|
||||
artifactBody := []byte("release-payload-1.0.0")
|
||||
uploadLocation := submitMultipartForm(t, router, "/admin/projects/"+projectID+"/releases", map[string]string{
|
||||
"version": "1.0.0",
|
||||
"build": "build-42",
|
||||
"release_notes": "Initial Windows stable rollout.",
|
||||
}, "artifact", "..\\Desktop App 1.0.0.zip", artifactBody, sessionCookie)
|
||||
|
||||
if !strings.Contains(uploadLocation, "status=release-uploaded") {
|
||||
t.Fatalf("expected release upload redirect status, got %q", uploadLocation)
|
||||
}
|
||||
|
||||
projectIDInt := mustParseInt64(t, projectID)
|
||||
project, err := store.Projects.GetByID(context.Background(), projectIDInt)
|
||||
if err != nil {
|
||||
t.Fatalf("load project: %v", err)
|
||||
}
|
||||
|
||||
if project.Slug != "desktop-app" {
|
||||
t.Fatalf("expected auto-generated project slug to persist, got %q", project.Slug)
|
||||
}
|
||||
|
||||
if !project.IsActive {
|
||||
t.Fatal("expected project to remain active until the archive action runs")
|
||||
}
|
||||
|
||||
projectTags, err := store.Projects.ListTags(context.Background(), project.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("load project tags: %v", err)
|
||||
}
|
||||
|
||||
if len(projectTags) != 1 || projectTags[0].Slug != "windows-stable" {
|
||||
t.Fatalf("expected one attached tag, got %+v", projectTags)
|
||||
}
|
||||
|
||||
releases, err := store.Releases.ListByProjectID(context.Background(), project.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("load releases: %v", err)
|
||||
}
|
||||
|
||||
if len(releases) != 1 {
|
||||
t.Fatalf("expected one release, got %d", len(releases))
|
||||
}
|
||||
|
||||
release := releases[0].Release
|
||||
if release.Filename != "desktop-app-1.0.0.zip" {
|
||||
t.Fatalf("expected sanitized filename, got %q", release.Filename)
|
||||
}
|
||||
|
||||
expectedChecksum := sha256.Sum256(artifactBody)
|
||||
if release.ChecksumSHA256 != hex.EncodeToString(expectedChecksum[:]) {
|
||||
t.Fatalf("expected checksum %s, got %s", hex.EncodeToString(expectedChecksum[:]), release.ChecksumSHA256)
|
||||
}
|
||||
|
||||
if release.StoragePath != "desktop-app/1.0.0/build-42/desktop-app-1.0.0.zip" {
|
||||
t.Fatalf("unexpected storage path %q", release.StoragePath)
|
||||
}
|
||||
|
||||
artifactPath := filepath.Join(cfg.ArtifactsDir, filepath.FromSlash(release.StoragePath))
|
||||
if strings.HasPrefix(artifactPath, cfg.StaticDir) {
|
||||
t.Fatalf("artifact path %q should not be inside static dir %q", artifactPath, cfg.StaticDir)
|
||||
}
|
||||
|
||||
storedArtifact, err := os.ReadFile(artifactPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read artifact from disk: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(storedArtifact, artifactBody) {
|
||||
t.Fatal("stored artifact body did not match uploaded payload")
|
||||
}
|
||||
|
||||
recorder := performRequest(t, router, http.MethodGet, "/admin/projects/"+projectID, nil, sessionCookie)
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("expected project detail page to load, got %d", recorder.Code)
|
||||
}
|
||||
|
||||
projectBody := recorder.Body.String()
|
||||
if !strings.Contains(projectBody, "/api/v1/projects/desktop-app/releases/latest") {
|
||||
t.Fatal("expected project detail page to show latest release metadata endpoint")
|
||||
}
|
||||
|
||||
if !strings.Contains(projectBody, "/api/v1/releases/1/download") {
|
||||
t.Fatal("expected project detail page to show release download endpoint")
|
||||
}
|
||||
|
||||
submitForm(t, router, http.MethodPost, "/admin/projects/"+projectID+"/archive", url.Values{
|
||||
"state": {"archive"},
|
||||
}, http.StatusSeeOther, sessionCookie)
|
||||
|
||||
project, err = store.Projects.GetByID(context.Background(), projectIDInt)
|
||||
if err != nil {
|
||||
t.Fatalf("reload project after archive: %v", err)
|
||||
}
|
||||
|
||||
if project.IsActive {
|
||||
t.Fatal("expected archived project to be inactive")
|
||||
}
|
||||
|
||||
recorder = performRequest(t, router, http.MethodGet, "/admin/projects", nil, sessionCookie)
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("expected projects page to load, got %d", recorder.Code)
|
||||
}
|
||||
|
||||
if !strings.Contains(recorder.Body.String(), "Desktop App") {
|
||||
t.Fatal("expected projects page to list the created project")
|
||||
}
|
||||
|
||||
recorder = performRequest(t, router, http.MethodGet, "/admin/tags", nil, sessionCookie)
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("expected tags page to load, got %d", recorder.Code)
|
||||
}
|
||||
|
||||
if !strings.Contains(recorder.Body.String(), "Windows Stable") {
|
||||
t.Fatal("expected tags page to list the created tag")
|
||||
}
|
||||
}
|
||||
|
||||
func loginAsAdmin(t *testing.T, router http.Handler) *http.Cookie {
|
||||
t.Helper()
|
||||
|
||||
csrfCookie := ensureCSRFCookie(t, router)
|
||||
recorder := performRequest(t, router, http.MethodPost, "/admin/login", url.Values{
|
||||
"email": {"admin@example.com"},
|
||||
"password": {"correct horse battery staple"},
|
||||
"next": {"/admin"},
|
||||
"csrf_token": {csrfCookie.Value},
|
||||
}, csrfCookie)
|
||||
|
||||
if recorder.Code != http.StatusSeeOther {
|
||||
t.Fatalf("expected login redirect, got %d", recorder.Code)
|
||||
}
|
||||
|
||||
for _, cookie := range recorder.Result().Cookies() {
|
||||
if cookie.Name == "update_server_session" {
|
||||
return cookie
|
||||
}
|
||||
}
|
||||
|
||||
t.Fatal("expected session cookie after login")
|
||||
return nil
|
||||
}
|
||||
|
||||
func submitForm(t *testing.T, router http.Handler, method, target string, form url.Values, expectedStatus int, cookies ...*http.Cookie) string {
|
||||
t.Helper()
|
||||
|
||||
if strings.HasPrefix(target, "/admin/") {
|
||||
csrfCookie := ensureCSRFCookie(t, router, cookies...)
|
||||
cookies = upsertCookie(cookies, csrfCookie)
|
||||
if form == nil {
|
||||
form = url.Values{}
|
||||
}
|
||||
if form.Get("csrf_token") == "" {
|
||||
form.Set("csrf_token", csrfCookie.Value)
|
||||
}
|
||||
}
|
||||
|
||||
recorder := performRequest(t, router, method, target, form, cookies...)
|
||||
if recorder.Code != expectedStatus {
|
||||
t.Fatalf("expected %d for %s %s, got %d with body %s", expectedStatus, method, target, recorder.Code, recorder.Body.String())
|
||||
}
|
||||
|
||||
return recorder.Header().Get("Location")
|
||||
}
|
||||
|
||||
func submitMultipartForm(t *testing.T, router http.Handler, target string, fields map[string]string, fileField, filename string, body []byte, cookies ...*http.Cookie) string {
|
||||
t.Helper()
|
||||
|
||||
if strings.HasPrefix(target, "/admin/") {
|
||||
csrfCookie := ensureCSRFCookie(t, router, cookies...)
|
||||
cookies = upsertCookie(cookies, csrfCookie)
|
||||
if fields == nil {
|
||||
fields = map[string]string{}
|
||||
}
|
||||
if fields["csrf_token"] == "" {
|
||||
fields["csrf_token"] = csrfCookie.Value
|
||||
}
|
||||
}
|
||||
|
||||
var requestBody bytes.Buffer
|
||||
writer := multipart.NewWriter(&requestBody)
|
||||
for key, value := range fields {
|
||||
if err := writer.WriteField(key, value); err != nil {
|
||||
t.Fatalf("write multipart field %s: %v", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
part, err := writer.CreateFormFile(fileField, filename)
|
||||
if err != nil {
|
||||
t.Fatalf("create multipart file part: %v", err)
|
||||
}
|
||||
|
||||
if _, err := part.Write(body); err != nil {
|
||||
t.Fatalf("write multipart file body: %v", err)
|
||||
}
|
||||
|
||||
if err := writer.Close(); err != nil {
|
||||
t.Fatalf("close multipart writer: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, target, &requestBody)
|
||||
req.RemoteAddr = "127.0.0.1:12345"
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
for _, cookie := range cookies {
|
||||
req.AddCookie(cookie)
|
||||
}
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, req)
|
||||
if recorder.Code != http.StatusSeeOther {
|
||||
t.Fatalf("expected multipart upload redirect, got %d with body %s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
|
||||
return recorder.Header().Get("Location")
|
||||
}
|
||||
|
||||
func upsertCookie(cookies []*http.Cookie, cookie *http.Cookie) []*http.Cookie {
|
||||
if cookie == nil {
|
||||
return cookies
|
||||
}
|
||||
|
||||
updated := make([]*http.Cookie, 0, len(cookies)+1)
|
||||
replaced := false
|
||||
for _, existing := range cookies {
|
||||
if existing == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if existing.Name == cookie.Name {
|
||||
updated = append(updated, cookie)
|
||||
replaced = true
|
||||
continue
|
||||
}
|
||||
|
||||
updated = append(updated, existing)
|
||||
}
|
||||
|
||||
if !replaced {
|
||||
updated = append(updated, cookie)
|
||||
}
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
func extractResourceID(t *testing.T, location, prefix string) string {
|
||||
t.Helper()
|
||||
|
||||
if !strings.HasPrefix(location, prefix) {
|
||||
t.Fatalf("expected redirect location with prefix %q, got %q", prefix, location)
|
||||
}
|
||||
|
||||
trimmed := strings.TrimPrefix(location, prefix)
|
||||
parts := strings.SplitN(trimmed, "?", 2)
|
||||
if parts[0] == "" {
|
||||
t.Fatalf("could not extract resource id from %q", location)
|
||||
}
|
||||
|
||||
return parts[0]
|
||||
}
|
||||
|
||||
func mustParseInt64(t *testing.T, raw string) int64 {
|
||||
t.Helper()
|
||||
|
||||
value, err := url.PathUnescape(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("unescape id %q: %v", raw, err)
|
||||
}
|
||||
|
||||
var parsed int64
|
||||
if _, err := fmt.Sscan(value, &parsed); err != nil {
|
||||
t.Fatalf("parse id %q: %v", raw, err)
|
||||
}
|
||||
|
||||
return parsed
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue