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()) } }