Files
yawn/server/app_test.go
T
Ampandheaust d930d8172f Serve sculpt studio at /sculpt
Keep existing viewer URLs compatible and test embedded pages, assets, and isolation headers.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0b4b4-202c-73f1-984d-9a55c88f9c83
Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
2026-09-28 10:25:02 +00:00

280 lines
8.8 KiB
Go

package main
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"time"
)
func testApplication(t *testing.T) *application {
t.Helper()
store, err := openPlaygroundStore(filepath.Join(t.TempDir(), "test.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { store.Close() })
monaco := filepath.Join(t.TempDir(), "monaco")
if err := os.MkdirAll(monaco, 0o755); err != nil {
t.Fatal(err)
}
app, err := newApplication(store, map[string]memoryAsset{
"core.js": {
contentType: "text/javascript; charset=utf-8",
content: []byte("export const ready = true"),
etag: `"core"`,
modified: time.Unix(0, 0),
},
}, monaco)
if err != nil {
t.Fatal(err)
}
return app
}
func TestPackageIsServedFromMemory(t *testing.T) {
request := httptest.NewRequest(http.MethodGet, "/pkg/core.js", nil)
response := httptest.NewRecorder()
testApplication(t).routes().ServeHTTP(response, request)
if response.Code != http.StatusOK {
t.Fatalf("status = %d", response.Code)
}
if response.Body.String() != "export const ready = true" {
t.Fatalf("body = %q", response.Body.String())
}
if response.Header().Get("Cross-Origin-Opener-Policy") != "same-origin" {
t.Fatal("cross-origin isolation header is missing")
}
if response.Header().Get("Access-Control-Allow-Origin") != "*" {
t.Fatal("package CORS header is missing")
}
if response.Header().Get("Cross-Origin-Resource-Policy") != "cross-origin" {
t.Fatal("package resource policy is missing")
}
}
func TestHealth(t *testing.T) {
request := httptest.NewRequest(http.MethodGet, "/healthz", nil)
response := httptest.NewRecorder()
testApplication(t).routes().ServeHTTP(response, request)
if response.Code != http.StatusNoContent {
t.Fatalf("status = %d", response.Code)
}
}
func TestHomeLogsDeploymentMetadata(t *testing.T) {
t.Setenv("YAWN_DEPLOYED_AT", "2026-08-24T18:30:00Z")
t.Setenv("YAWN_DEPLOY_COMMIT_MESSAGE", "Fix Docker deployment")
request := httptest.NewRequest(http.MethodGet, "/", nil)
response := httptest.NewRecorder()
testApplication(t).routes().ServeHTTP(response, request)
if response.Code != http.StatusOK {
t.Fatalf("status = %d", response.Code)
}
for _, expected := range []string{
`console.log("Yawn deployed at 25 August 2026, 12:00:00 AM IST\nLatest commit: Fix Docker deployment")`,
"<title>Yawn",
} {
if !bytes.Contains(response.Body.Bytes(), []byte(expected)) {
t.Fatalf("home page does not contain %q", expected)
}
}
}
func TestPlaygroundStoreMigratesLoadouts(t *testing.T) {
path := filepath.Join(t.TempDir(), "legacy.db")
database, err := sql.Open("sqlite3", path)
if err != nil {
t.Fatal(err)
}
if _, err := database.Exec(`
CREATE TABLE playgrounds (
id TEXT NOT NULL,
revision INTEGER NOT NULL,
title TEXT NOT NULL,
code TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY (id, revision)
)
`); err != nil {
database.Close()
t.Fatal(err)
}
if err := database.Close(); err != nil {
t.Fatal(err)
}
store, err := openPlaygroundStore(path)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { store.Close() })
saved, err := store.save(context.Background(), "", "Migrated", "const migrated = true;", "")
if err != nil {
t.Fatal(err)
}
if string(saved.Loadouts) != "[]" {
t.Fatalf("loadouts = %s", saved.Loadouts)
}
}
func TestSaveAndLoadPlaygroundRevision(t *testing.T) {
app := testApplication(t)
handler := app.routes()
requestBody, _ := json.Marshal(map[string]any{
"title": "First scene",
"code": "const scene = true;",
"loadouts": []map[string]any{{
"id": "custom", "name": "Custom", "layout": map[string]any{},
}},
})
request := httptest.NewRequest(http.MethodPost, "/api/playgrounds", bytes.NewReader(requestBody))
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if response.Code != http.StatusCreated {
t.Fatalf("save status = %d: %s", response.Code, response.Body.String())
}
var saved playground
if err := json.NewDecoder(response.Body).Decode(&saved); err != nil {
t.Fatal(err)
}
if saved.Revision != 1 || !playgroundIDPattern.MatchString(saved.ID) {
t.Fatalf("saved = %#v", saved)
}
requestBody, _ = json.Marshal(map[string]string{
"id": saved.ID,
"title": "Second scene",
"code": "const scene = false;",
})
request = httptest.NewRequest(http.MethodPost, "/api/playgrounds", bytes.NewReader(requestBody))
response = httptest.NewRecorder()
handler.ServeHTTP(response, request)
if response.Code != http.StatusCreated {
t.Fatalf("second save status = %d: %s", response.Code, response.Body.String())
}
var second playground
if err := json.NewDecoder(response.Body).Decode(&second); err != nil {
t.Fatal(err)
}
if second.ID != saved.ID || second.Revision != 2 {
t.Fatalf("second = %#v", second)
}
request = httptest.NewRequest(http.MethodGet, "/api/playgrounds/"+saved.ID+"/1", nil)
response = httptest.NewRecorder()
handler.ServeHTTP(response, request)
if response.Code != http.StatusOK {
t.Fatalf("load status = %d: %s", response.Code, response.Body.String())
}
var loaded playground
if err := json.NewDecoder(response.Body).Decode(&loaded); err != nil {
t.Fatal(err)
}
if loaded.Code != "const scene = true;" || loaded.Title != "First scene" {
t.Fatalf("loaded = %#v", loaded)
}
var loadouts []struct {
ID string `json:"id"`
Name string `json:"name"`
Layout map[string]any `json:"layout"`
}
if err := json.Unmarshal(loaded.Loadouts, &loadouts); err != nil {
t.Fatal(err)
}
if len(loadouts) != 1 || loadouts[0].ID != "custom" || loadouts[0].Name != "Custom" || len(loadouts[0].Layout) != 0 {
t.Fatalf("loadouts = %s", loaded.Loadouts)
}
request = httptest.NewRequest(http.MethodGet, "/api/playgrounds/"+saved.ID+"/2", nil)
response = httptest.NewRecorder()
handler.ServeHTTP(response, request)
if response.Code != http.StatusOK {
t.Fatalf("second load status = %d: %s", response.Code, response.Body.String())
}
if err := json.NewDecoder(response.Body).Decode(&loaded); err != nil {
t.Fatal(err)
}
if loaded.Code != "const scene = false;" || loaded.Title != "Second scene" {
t.Fatalf("second loaded = %#v", loaded)
}
}
func TestSavedPlaygroundPageRoute(t *testing.T) {
request := httptest.NewRequest(http.MethodGet, "/playground/abcdef12/1", nil)
response := httptest.NewRecorder()
testApplication(t).routes().ServeHTTP(response, request)
if response.Code != http.StatusOK {
t.Fatalf("status = %d", response.Code)
}
if contentType := response.Header().Get("Content-Type"); contentType != "text/html; charset=utf-8" {
t.Fatalf("content type = %q", contentType)
}
}
func TestSculptRoutesAndAssets(t *testing.T) {
routes := testApplication(t).routes()
for _, path := range []string{"/sculpt", "/sculpt/", "/view/sculpt", "/view/sculpt/", "/assets/sculpt/app.js", "/assets/sculpt/styles.css"} {
t.Run(path, func(t *testing.T) {
response := httptest.NewRecorder()
routes.ServeHTTP(response, httptest.NewRequest(http.MethodGet, path, nil))
if response.Code != http.StatusOK {
t.Fatalf("status = %d", response.Code)
}
for name, expected := range map[string]string{
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
} {
if got := response.Header().Get(name); got != expected {
t.Fatalf("%s = %q, want %q", name, got, expected)
}
}
switch path {
case "/assets/sculpt/app.js", "/assets/sculpt/styles.css":
expected := "text/javascript; charset=utf-8"
if filepath.Ext(path) == ".css" {
expected = "text/css; charset=utf-8"
}
if response.Header().Get("Content-Type") != expected || response.Body.Len() == 0 {
t.Fatalf("missing asset or wrong content type: %q", response.Header().Get("Content-Type"))
}
default:
if got := response.Header().Get("Content-Type"); got != "text/html; charset=utf-8" {
t.Fatalf("content type = %q", got)
}
for _, content := range []string{"<title>Sculpt — Yawn</title>", `src="/assets/sculpt/app.js"`, `href="/assets/sculpt/styles.css"`, `"@yawn/core": "/pkg/core.js"`} {
if !bytes.Contains(response.Body.Bytes(), []byte(content)) {
t.Fatalf("sculpt page missing %q", content)
}
}
}
})
}
}
func TestGltfViewerPageRoute(t *testing.T) {
request := httptest.NewRequest(http.MethodGet, "/view/gltf", nil)
response := httptest.NewRecorder()
testApplication(t).routes().ServeHTTP(response, request)
if response.Code != http.StatusOK {
t.Fatalf("status = %d", response.Code)
}
if contentType := response.Header().Get("Content-Type"); contentType != "text/html; charset=utf-8" {
t.Fatalf("content type = %q", contentType)
}
for _, content := range []string{
`"@yawn/core": "/pkg/core.js"`,
`"@yawn/handles": "/pkg/handles.js"`,
"Drop a glTF file",
} {
if !bytes.Contains(response.Body.Bytes(), []byte(content)) {
t.Fatalf("viewer page does not contain %q", content)
}
}
}