Files
yawn/server/app_test.go
T
Ampandheaust d71da85307 Fix Docker deployment inputs
Include all addon workspaces and vendored dependencies in image builds, and expose deploy time plus commit metadata on the home page.

Amp-Thread-ID: https://ampcode.com/threads/T-01a033d4-0e44-708e-af0e-6166283416ff
Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
2026-08-24 18:30:45 +00:00

240 lines
7.2 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 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)
}
}
}