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