The manual the binary carries, rendered on the client port: a sidebar of the documents in the console's reading order (now one list, llmgw.Manual, for both), heading anchors, links between documents kept in the portal and links to files outside the binary sent to the repository, no script. Public by default; landing.docs_sign_in sends visitors to sign in first. docs/clients.md shows the official OpenAI and Anthropic SDKs against llmgw in Python, TypeScript, Go and curl, with llmgw's headers; the portal's index, the landing page and the account page link to it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
175 lines
6.3 KiB
Go
175 lines
6.3 KiB
Go
package web
|
|
|
|
import (
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
llmgw "gitea.mynodes.xyz/ilker/llmgw"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/store"
|
|
)
|
|
|
|
// docsMux mounts the portal the way the router does.
|
|
func docsMux(h http.HandlerFunc) *http.ServeMux {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("GET /docs", h)
|
|
mux.HandleFunc("GET /docs/{doc}", h)
|
|
return mux
|
|
}
|
|
|
|
func TestTheDocsPortalRendersADocumentWithItsHeadingsAndCode(t *testing.T) {
|
|
code, body := get(t, docsMux(Docs("v1", false)), "/docs/clients", nil)
|
|
if code != http.StatusOK {
|
|
t.Fatalf("status %d", code)
|
|
}
|
|
for _, want := range []string{
|
|
`<h1 id="using-llmgw-from-code">Using llmgw from code`,
|
|
// A heading links to itself, for someone sending a section to someone.
|
|
`<h2 id="streaming">Streaming<a href="#streaming"`,
|
|
// Fenced code keeps its language, and its text is escaped, not markup.
|
|
`<pre><code class="language-python">`,
|
|
`client.chat.completions.create(`,
|
|
// Tables are GitHub's.
|
|
`<table>`,
|
|
// The sidebar offers every document and marks the open one.
|
|
`<a href="/docs/clients" aria-current="page">Using llmgw from code</a>`,
|
|
`<a href="/docs/reference">`,
|
|
// And this document's sections beneath it.
|
|
`<a href="#the-anthropic-sdk">The Anthropic SDK</a>`,
|
|
} {
|
|
if !strings.Contains(body, want) {
|
|
t.Errorf("the page lacks %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEveryDocumentInTheManualIsAPage(t *testing.T) {
|
|
h := docsMux(Docs("v1", false))
|
|
for _, d := range llmgw.Manual() {
|
|
if code, _ := get(t, h, "/docs/"+d.Key, nil); code != http.StatusOK {
|
|
t.Errorf("/docs/%s: %d", d.Key, code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTheIndexLeadsWithTheClientGuide(t *testing.T) {
|
|
code, body := get(t, docsMux(Docs("v1", true)), "/docs", nil)
|
|
if code != http.StatusOK {
|
|
t.Fatalf("status %d", code)
|
|
}
|
|
lead := strings.Index(body, `<a href="/docs/clients">Using llmgw from code</a> shows`)
|
|
every := strings.Index(body, "Every document")
|
|
if lead < 0 || every < 0 || lead > every {
|
|
t.Errorf("the client guide is not offered first:\n%s", body)
|
|
}
|
|
if !strings.Contains(body, `<a href="/login">Sign in</a>`) {
|
|
t.Error("a gateway with accounts does not offer to sign in")
|
|
}
|
|
}
|
|
|
|
func TestLinksBetweenDocumentsStayInThePortal(t *testing.T) {
|
|
paths := map[string]string{"docs/install.md": "install", "docs/guardrails.md": "guardrails",
|
|
"README.md": "readme", "CHANGELOG.md": "changes"}
|
|
src := "See [install](install.md), [a section](install.md#2-the-control-plane), " +
|
|
"[here](#local), [the readme](../README.md), [proxies](../deploy/proxy/), " +
|
|
"[the plan](llmgw-plan.md) and [elsewhere](https://example.com/x.md).\n"
|
|
body, _ := renderDoc(src, "docs/guardrails.md", paths)
|
|
for _, want := range []string{
|
|
`href="/docs/install"`,
|
|
`href="/docs/install#2-the-control-plane"`,
|
|
`href="#local"`,
|
|
`href="/docs/readme"`,
|
|
// Not in the binary: the repository has it.
|
|
`href="` + repoURL + `/src/branch/main/deploy/proxy/"`,
|
|
`href="` + repoURL + `/src/branch/main/docs/llmgw-plan.md"`,
|
|
`href="https://example.com/x.md"`,
|
|
} {
|
|
if !strings.Contains(string(body), want) {
|
|
t.Errorf("rendered without %s:\n%s", want, body)
|
|
}
|
|
}
|
|
// The README links into docs/ from the repository's root.
|
|
body, _ = renderDoc("[guardrails](docs/guardrails.md)\n", "README.md", paths)
|
|
if !strings.Contains(string(body), `href="/docs/guardrails"`) {
|
|
t.Errorf("a link from the README: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestNoEmbeddedDocumentLinksToAPageThePortalLacks(t *testing.T) {
|
|
// Every link a document makes into /docs/ must land on a page, or the
|
|
// portal ships its own 404s.
|
|
keys := map[string]bool{}
|
|
for _, d := range manual() {
|
|
keys[d.Key] = true
|
|
}
|
|
for _, d := range manual() {
|
|
for _, part := range strings.Split(string(d.Body), `href="/docs/`)[1:] {
|
|
key := part[:strings.IndexAny(part, `"#`)]
|
|
if !keys[key] {
|
|
t.Errorf("%s links to /docs/%s, which is not a page", d.Key, key)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAnUnknownDocumentIsNotFound(t *testing.T) {
|
|
h := docsMux(Docs("v1", false))
|
|
for _, path := range []string{"/docs/no-such-doc", "/docs/..%2fgo.mod", "/docs/BACKLOG"} {
|
|
if code, _ := get(t, h, path, nil); code != http.StatusNotFound {
|
|
t.Errorf("%s: %d, want 404", path, code)
|
|
}
|
|
}
|
|
// The file name works as well as the key: it is what the documents say.
|
|
if code, _ := get(t, h, "/docs/install.md", nil); code != http.StatusOK {
|
|
t.Errorf("/docs/install.md: %d", code)
|
|
}
|
|
}
|
|
|
|
func TestTheDocsCarryThePolicyAndNoScript(t *testing.T) {
|
|
for _, path := range []string{"/docs", "/docs/clients", "/docs/reference", "/docs/readme"} {
|
|
rec := httptest.NewRecorder()
|
|
docsMux(Docs("v1", false)).ServeHTTP(rec, httptest.NewRequest("GET", path, nil))
|
|
csp := rec.Header().Get("Content-Security-Policy")
|
|
if !strings.Contains(csp, "script-src 'self';") || strings.Contains(csp, "sha256-") {
|
|
t.Errorf("%s: policy %q, want this server's scripts and no inline one", path, csp)
|
|
}
|
|
if strings.Contains(strings.ToLower(rec.Body.String()), "<script") {
|
|
t.Errorf("%s emits a script", path)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTheDocsCanRequireSigningIn(t *testing.T) {
|
|
a := fakeAccounts{user: store.User{ID: "u1", Email: "a@example.com"}}
|
|
p := NewAccountPages(a, nil, nil, "v0", slog.New(slog.NewTextHandler(io.Discard, nil)))
|
|
|
|
required := docsMux(p.DocsPage(Docs("v0", true), true))
|
|
rec := httptest.NewRecorder()
|
|
required.ServeHTTP(rec, httptest.NewRequest("GET", "/docs/install", nil))
|
|
if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/login" {
|
|
t.Errorf("without a session: %d to %q, want a redirect to /login", rec.Code, rec.Header().Get("Location"))
|
|
}
|
|
signedIn := mirrorGet(t, required, "/docs/install")
|
|
if signedIn.Code != http.StatusOK {
|
|
t.Fatalf("signed in: %d", signedIn.Code)
|
|
}
|
|
// Signed in, the page draws the portal's menu with Docs marked.
|
|
if menu := menuRe.FindString(signedIn.Body.String()); !strings.Contains(menu, `aria-current="page">Docs<`) ||
|
|
!strings.Contains(menu, `"/logout"`) {
|
|
t.Errorf("the signed-in menu:\n%s", menu)
|
|
}
|
|
if !strings.Contains(signedIn.Header().Get("Cache-Control"), "no-store") {
|
|
t.Error("a page with someone's menu on it may be cached")
|
|
}
|
|
|
|
public := docsMux(p.DocsPage(Docs("v0", true), false))
|
|
rec = httptest.NewRecorder()
|
|
public.ServeHTTP(rec, httptest.NewRequest("GET", "/docs/install", nil))
|
|
if rec.Code != http.StatusOK || strings.Contains(rec.Body.String(), `"/logout"`) {
|
|
t.Errorf("public, without a session: %d", rec.Code)
|
|
}
|
|
}
|