Files
lrc-local/server_test.go
T

119 lines
3.9 KiB
Go

package main
import (
"io/fs"
"net/http"
"net/http/httptest"
"strings"
"testing"
"testing/fstest"
)
type fakeQQ struct{}
func (fakeQQ) Search(keyword string, limit int) ([]song, error) {
return []song{{Name: keyword, MID: "0004jPDk2eB2dt", Singer: "Tester", Interval: limit}}, nil
}
func (fakeQQ) Lyrics(string) (lyricResult, error) {
return lyricResult{Code: 0, Lyric: "[00:01.00]line", Trans: "[00:01.00]翻译", Roma: "[00:01.00]sound"}, nil
}
func testApp() (*localApp, http.Handler, fs.FS) {
web := fstest.MapFS{
"app.html": &fstest.MapFile{Data: []byte("app")},
"app.js": &fstest.MapFile{Data: []byte("js")},
"styles.css": &fstest.MapFile{Data: []byte("css")},
}
app := newApp("http://127.0.0.1:18765", "test-secret", web, fakeQQ{})
return app, app.routes(), web
}
func request(method, target, body string) *http.Request {
r := httptest.NewRequest(method, target, strings.NewReader(body))
r.Host = "127.0.0.1:18765"
return r
}
func validSessionCookie() *http.Cookie {
return &http.Cookie{Name: sessionCookie, Value: "test-secret"}
}
func TestLaunchTokenCreatesStrictSession(t *testing.T) {
_, handler, _ := testApp()
w := httptest.NewRecorder()
handler.ServeHTTP(w, request(http.MethodGet, "http://127.0.0.1:18765/start?t=test-secret", ""))
if w.Code != http.StatusSeeOther {
t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
}
cookies := w.Result().Cookies()
if len(cookies) != 1 || !cookies[0].HttpOnly || cookies[0].SameSite != http.SameSiteStrictMode {
t.Fatalf("unsafe cookie: %#v", cookies)
}
}
func TestRejectsWrongHost(t *testing.T) {
_, handler, _ := testApp()
r := request(http.MethodGet, "http://127.0.0.1:18765/health", "")
r.Host = "attacker.example"
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusForbidden {
t.Fatalf("expected 403, got %d", w.Code)
}
}
func TestRejectsCrossOriginAPI(t *testing.T) {
_, handler, _ := testApp()
r := request(http.MethodPost, "http://127.0.0.1:18765/api/search", `{"keyword":"test","limit":10}`)
r.AddCookie(validSessionCookie())
r.Header.Set("Content-Type", "application/json")
r.Header.Set("Origin", "https://attacker.example")
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusForbidden {
t.Fatalf("expected 403, got %d", w.Code)
}
}
func TestValidSearchAndSecurityHeaders(t *testing.T) {
_, handler, _ := testApp()
r := request(http.MethodPost, "http://127.0.0.1:18765/api/search", `{"keyword":"测试","limit":10}`)
r.AddCookie(validSessionCookie())
r.Header.Set("Content-Type", "application/json")
r.Header.Set("Origin", "http://127.0.0.1:18765")
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), "测试") {
t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
}
if !strings.Contains(w.Header().Get("Content-Security-Policy"), "default-src 'none'") {
t.Fatal("strict CSP is missing")
}
}
func TestRejectsInvalidMID(t *testing.T) {
_, handler, _ := testApp()
r := request(http.MethodPost, "http://127.0.0.1:18765/api/lyrics", `{"mid":"https://attacker.example/"}`)
r.AddCookie(validSessionCookie())
r.Header.Set("Content-Type", "application/json")
r.Header.Set("Origin", "http://127.0.0.1:18765")
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", w.Code)
}
}
func TestLyricsReturnsAvailableTranslationAndPhonetics(t *testing.T) {
_, handler, _ := testApp()
r := request(http.MethodPost, "http://127.0.0.1:18765/api/lyrics", `{"mid":"0004jPDk2eB2dt"}`)
r.AddCookie(validSessionCookie())
r.Header.Set("Content-Type", "application/json")
r.Header.Set("Origin", "http://127.0.0.1:18765")
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), `"trans":"[00:01.00]翻译"`) || !strings.Contains(w.Body.String(), `"roma":"[00:01.00]sound"`) {
t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
}
}