Files
at-container-registry/pkg/appview/readme/fetcher_test.go
T
2026-01-04 22:02:01 -06:00

579 lines
15 KiB
Go

package readme
import (
"context"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
)
func TestGetBaseURL(t *testing.T) {
tests := []struct {
name string
inputURL string
expected string
}{
{
name: "nil URL",
inputURL: "",
expected: "",
},
{
name: "GitHub raw URL",
inputURL: "https://raw.githubusercontent.com/user/repo/main/README.md",
expected: "https://github.com/user/repo/blob/main/",
},
{
name: "GitHub raw URL with subdirectory",
inputURL: "https://raw.githubusercontent.com/user/repo/main/docs/README.md",
expected: "https://github.com/user/repo/blob/main/",
},
{
name: "GitHub raw URL with branch",
inputURL: "https://raw.githubusercontent.com/user/repo/develop/README.md",
expected: "https://github.com/user/repo/blob/develop/",
},
{
name: "regular URL",
inputURL: "https://example.com/docs/README.md",
expected: "https://example.com/docs/",
},
{
name: "URL with multiple path segments",
inputURL: "https://example.com/path/to/docs/README.md",
expected: "https://example.com/path/to/docs/",
},
{
name: "URL with root file",
inputURL: "https://example.com/README.md",
expected: "https://example.com/",
},
{
name: "URL without file",
inputURL: "https://example.com/docs/",
expected: "https://example.com/docs/",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var u *url.URL
if tt.inputURL != "" {
var err error
u, err = url.Parse(tt.inputURL)
if err != nil {
t.Fatalf("Failed to parse URL %q: %v", tt.inputURL, err)
}
}
result := getBaseURL(u)
if result != tt.expected {
t.Errorf("getBaseURL(%q) = %q, want %q", tt.inputURL, result, tt.expected)
}
})
}
}
func TestRewriteRelativeURLs(t *testing.T) {
tests := []struct {
name string
html string
baseURL string
expected string
}{
{
name: "empty baseURL",
html: `<img src="./image.png">`,
baseURL: "",
expected: `<img src="./image.png">`,
},
{
name: "invalid baseURL",
html: `<img src="./image.png">`,
baseURL: "://invalid",
expected: `<img src="./image.png">`,
},
{
name: "current directory relative src",
html: `<img src="./image.png">`,
baseURL: "https://example.com/docs/",
expected: `<img src="https://example.com/docs/image.png">`,
},
{
name: "current directory relative href",
html: `<a href="./page.html">link</a>`,
baseURL: "https://example.com/docs/",
expected: `<a href="https://example.com/docs/page.html">link</a>`,
},
{
name: "parent directory relative src",
html: `<img src="../image.png">`,
baseURL: "https://example.com/docs/",
expected: `<img src="https://example.com/docs/../image.png">`,
},
{
name: "parent directory relative href",
html: `<a href="../page.html">link</a>`,
baseURL: "https://example.com/docs/",
expected: `<a href="https://example.com/docs/../page.html">link</a>`,
},
{
name: "root-relative src",
html: `<img src="/images/logo.png">`,
baseURL: "https://example.com/docs/",
expected: `<img src="https://example.com/images/logo.png">`,
},
{
name: "root-relative href",
html: `<a href="/about">link</a>`,
baseURL: "https://example.com/docs/",
expected: `<a href="https://example.com/about">link</a>`,
},
{
name: "mixed relative URLs",
html: `<img src="./img.png"><a href="../page.html">link</a>`,
baseURL: "https://example.com/docs/",
expected: `<img src="https://example.com/docs/img.png"><a href="https://example.com/docs/../page.html">link</a>`,
},
{
name: "absolute URLs unchanged",
html: `<img src="https://cdn.example.com/image.png">`,
baseURL: "https://example.com/docs/",
expected: `<img src="https://cdn.example.com/image.png">`,
},
{
name: "protocol-relative URLs (incorrectly converted)",
html: `<img src="//cdn.example.com/image.png">`,
baseURL: "https://example.com/docs/",
expected: `<img src="https://example.com//cdn.example.com/image.png">`,
},
{
name: "bare relative src (no ./ prefix)",
html: `<img src="image.png">`,
baseURL: "https://example.com/docs/",
expected: `<img src="https://example.com/docs/image.png">`,
},
{
name: "bare relative href (no ./ prefix)",
html: `<a href="page.html">link</a>`,
baseURL: "https://example.com/docs/",
expected: `<a href="https://example.com/docs/page.html">link</a>`,
},
{
name: "bare relative with path",
html: `<img src="images/logo.png">`,
baseURL: "https://example.com/docs/",
expected: `<img src="https://example.com/docs/images/logo.png">`,
},
{
name: "anchor links unchanged",
html: `<a href="#section">link</a>`,
baseURL: "https://example.com/docs/",
expected: `<a href="#section">link</a>`,
},
{
name: "data URLs unchanged",
html: `<img src="data:image/png;base64,abc123">`,
baseURL: "https://example.com/docs/",
expected: `<img src="data:image/png;base64,abc123">`,
},
{
name: "mailto links unchanged",
html: `<a href="mailto:test@example.com">email</a>`,
baseURL: "https://example.com/docs/",
expected: `<a href="mailto:test@example.com">email</a>`,
},
{
name: "mixed bare and prefixed relative URLs",
html: `<img src="slices_and_lucy.png"><a href="./other.md">link</a>`,
baseURL: "https://github.com/user/repo/blob/main/",
expected: `<img src="https://github.com/user/repo/blob/main/slices_and_lucy.png"><a href="https://github.com/user/repo/blob/main/other.md">link</a>`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := rewriteRelativeURLs(tt.html, tt.baseURL)
if result != tt.expected {
t.Errorf("rewriteRelativeURLs() = %q, want %q", result, tt.expected)
}
})
}
}
func TestFetcher_RenderMarkdown(t *testing.T) {
fetcher := NewFetcher()
tests := []struct {
name string
content string
wantContain string
wantErr bool
}{
{
name: "simple paragraph",
content: "Hello, world!",
wantContain: "<p>Hello, world!</p>",
wantErr: false,
},
{
name: "heading",
content: "# My App",
wantContain: "<h1",
wantErr: false,
},
{
name: "bold text",
content: "This is **bold** text.",
wantContain: "<strong>bold</strong>",
wantErr: false,
},
{
name: "italic text",
content: "This is *italic* text.",
wantContain: "<em>italic</em>",
wantErr: false,
},
{
name: "code block",
content: "```\ncode here\n```",
wantContain: "<pre>",
wantErr: false,
},
{
name: "link",
content: "[Link text](https://example.com)",
wantContain: `href="https://example.com"`,
wantErr: false,
},
{
name: "image",
content: "![Alt text](https://example.com/image.png)",
wantContain: `src="https://example.com/image.png"`,
wantErr: false,
},
{
name: "unordered list",
content: "- Item 1\n- Item 2",
wantContain: "<ul>",
wantErr: false,
},
{
name: "ordered list",
content: "1. Item 1\n2. Item 2",
wantContain: "<ol>",
wantErr: false,
},
{
name: "empty content",
content: "",
wantContain: "",
wantErr: false,
},
{
name: "complex markdown",
content: "# Title\n\nA paragraph with **bold** and *italic* text.\n\n- List item 1\n- List item 2\n\n```go\nfunc main() {}\n```",
wantContain: "<h1",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
html, err := fetcher.RenderMarkdown([]byte(tt.content))
if (err != nil) != tt.wantErr {
t.Errorf("RenderMarkdown() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && tt.wantContain != "" {
if !containsSubstring(html, tt.wantContain) {
t.Errorf("RenderMarkdown() = %q, want to contain %q", html, tt.wantContain)
}
}
})
}
}
func containsSubstring(s, substr string) bool {
return len(substr) == 0 || (len(s) >= len(substr) && (s == substr || len(s) > 0 && containsSubstringHelper(s, substr)))
}
func containsSubstringHelper(s, substr string) bool {
for i := range len(s) - len(substr) + 1 {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
func TestLooksLikeHTML(t *testing.T) {
tests := []struct {
name string
content string
expected bool
}{
{
name: "empty content",
content: "",
expected: false,
},
{
name: "markdown content",
content: "# Hello World\n\nThis is a README.",
expected: false,
},
{
name: "plain text",
content: "Just some plain text without any HTML.",
expected: false,
},
{
name: "doctype html",
content: "<!DOCTYPE html>\n<html><body>Page</body></html>",
expected: true,
},
{
name: "doctype html lowercase",
content: "<!doctype html>\n<html><body>Page</body></html>",
expected: true,
},
{
name: "html tag only",
content: "<html><head></head><body>Page</body></html>",
expected: true,
},
{
name: "html tag with whitespace",
content: " \n <html>\n<body>Page</body></html>",
expected: true,
},
{
name: "xml declaration",
content: "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<html>...</html>",
expected: true,
},
{
name: "soft 404 page",
content: "<!DOCTYPE html><html><head><title>Page Not Found</title></head><body><h1>404</h1></body></html>",
expected: true,
},
{
name: "markdown with inline html",
content: "# Title\n\nSome text with <strong>bold</strong> inline.",
expected: false,
},
{
name: "markdown starting with hash",
content: "## Section\n\nContent here.",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := LooksLikeHTML([]byte(tt.content))
if result != tt.expected {
t.Errorf("looksLikeHTML(%q) = %v, want %v", tt.content, result, tt.expected)
}
})
}
}
func TestFetcher_FetchRaw(t *testing.T) {
fetcher := NewFetcher()
tests := []struct {
name string
handler http.HandlerFunc
wantErr bool
errContains string
wantContent string
}{
{
name: "successful markdown fetch",
handler: func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("# Hello World\n\nThis is markdown."))
},
wantErr: false,
wantContent: "# Hello World",
},
{
name: "rejects HTML content type",
handler: func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte("<html><body>Error</body></html>"))
},
wantErr: true,
errContains: "unsupported content type",
},
{
name: "rejects soft 404 HTML content",
handler: func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("<!DOCTYPE html><html><body>404 Not Found</body></html>"))
},
wantErr: true,
errContains: "detected HTML content",
},
{
name: "rejects 404 status",
handler: func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("Not Found"))
},
wantErr: true,
errContains: "unexpected status code: 404",
},
{
name: "rejects 500 status",
handler: func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Internal Server Error"))
},
wantErr: true,
errContains: "unexpected status code: 500",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(tt.handler)
defer server.Close()
content, err := fetcher.FetchRaw(context.Background(), server.URL)
if tt.wantErr {
if err == nil {
t.Errorf("FetchRaw() expected error containing %q, got nil", tt.errContains)
return
}
if !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("FetchRaw() error = %q, want error containing %q", err.Error(), tt.errContains)
}
return
}
if err != nil {
t.Errorf("FetchRaw() unexpected error: %v", err)
return
}
if !strings.Contains(string(content), tt.wantContent) {
t.Errorf("FetchRaw() content = %q, want content containing %q", string(content), tt.wantContent)
}
})
}
}
func TestFetcher_FetchRaw_URLValidation(t *testing.T) {
fetcher := NewFetcher()
tests := []struct {
name string
url string
errContains string
}{
{
name: "empty URL",
url: "",
errContains: "empty README URL",
},
{
name: "invalid URL scheme",
url: "ftp://example.com/README.md",
errContains: "invalid URL scheme",
},
{
name: "file URL scheme",
url: "file:///etc/passwd",
errContains: "invalid URL scheme",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := fetcher.FetchRaw(context.Background(), tt.url)
if err == nil {
t.Errorf("FetchRaw(%q) expected error, got nil", tt.url)
return
}
if !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("FetchRaw(%q) error = %q, want error containing %q", tt.url, err.Error(), tt.errContains)
}
})
}
}
func TestFetcher_FetchAndRender(t *testing.T) {
fetcher := NewFetcher()
tests := []struct {
name string
handler http.HandlerFunc
wantErr bool
errContains string
wantContain string
}{
{
name: "renders markdown to HTML",
handler: func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("# Hello World\n\nThis is **bold** text."))
},
wantErr: false,
wantContain: "<strong>bold</strong>",
},
{
name: "rejects HTML content type",
handler: func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.Write([]byte("<html><body>Error</body></html>"))
},
wantErr: true,
errContains: "unsupported content type",
},
{
name: "rejects soft 404",
handler: func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("<!doctype html><html><body>Not Found</body></html>"))
},
wantErr: true,
errContains: "detected HTML content",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(tt.handler)
defer server.Close()
html, err := fetcher.FetchAndRender(context.Background(), server.URL)
if tt.wantErr {
if err == nil {
t.Errorf("FetchAndRender() expected error containing %q, got nil", tt.errContains)
return
}
if !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("FetchAndRender() error = %q, want error containing %q", err.Error(), tt.errContains)
}
return
}
if err != nil {
t.Errorf("FetchAndRender() unexpected error: %v", err)
return
}
if !strings.Contains(html, tt.wantContain) {
t.Errorf("FetchAndRender() = %q, want HTML containing %q", html, tt.wantContain)
}
})
}
}