package app import ( "bufio" "context" "encoding/json" "errors" "fmt" "os" "path/filepath" "runtime" "strings" "sync" "testing" ) func readDiagnosticEvents(t *testing.T, path string) []diagnosticEvent { t.Helper() file, err := os.Open(path) if err != nil { t.Fatal(err) } defer file.Close() var events []diagnosticEvent scanner := bufio.NewScanner(file) for scanner.Scan() { var event diagnosticEvent if err := json.Unmarshal(scanner.Bytes(), &event); err != nil || event.Time.IsZero() || event.Event == "" { t.Fatal("invalid diagnostic record", err) } events = append(events, event) } if err := scanner.Err(); err != nil { t.Fatal(err) } return events } func TestDiagnosticLogConcurrentWritesAndRedaction(t *testing.T) { log := newDiagnosticLog(t.TempDir()) d := &Downloader{diagnostics: log} var group sync.WaitGroup for index := 0; index < 50; index++ { group.Add(1) go func(index int) { defer group.Done() d.recordDiagnostic(diagnosticEvent{Event: "fixture.failed", Episode: index + 1, Message: "https://user:fixture-password@media.invalid/video?token=fixture-token&key=fixture-media-key&signature=fixture-signature#fixture-fragment"}) }(index) } group.Wait() events := readDiagnosticEvents(t, log.path) seen := make(map[int]bool) for _, event := range events { seen[event.Episode] = true for _, secret := range []string{"fixture-token", "fixture-media-key", "fixture-password", "fixture-signature", "fixture-fragment"} { if strings.Contains(event.Message, secret) { t.Fatal("diagnostic log exposed a credential or signed URL parameter") } } } if len(events) != 50 || len(seen) != 50 { t.Fatal("concurrent diagnostic events were lost or interleaved") } info, err := os.Stat(log.path) if err != nil || runtime.GOOS != "windows" && info.Mode().Perm()&0077 != 0 { t.Fatal("new diagnostic file was not private", err) } } func TestDiagnosticLogRotationBoundsFiles(t *testing.T) { log := newDiagnosticLog(t.TempDir()) log.limit = 512 for index := 0; index < 30; index++ { if err := log.write(diagnosticEvent{Event: "fixture.rotate", Episode: index, Message: strings.Repeat("x", 100)}); err != nil { t.Fatal(err) } } files, err := os.ReadDir(filepath.Dir(log.path)) if err != nil || len(files) != diagnosticLogBackups+1 { t.Fatal("diagnostic rotation exceeded its file bound", err) } for _, file := range files { info, err := file.Info() if err != nil || info.Size() > log.limit || len(readDiagnosticEvents(t, filepath.Join(filepath.Dir(log.path), file.Name()))) == 0 { t.Fatal("rotation kept an oversized or invalid file", err) } } current := readDiagnosticEvents(t, log.path) if current[len(current)-1].Episode != 29 { t.Fatal("rotation discarded the latest event") } if err := newDiagnosticLog(filepath.Dir(filepath.Dir(log.path))).write(diagnosticEvent{Event: "fixture.restart", Message: "restart"}); err != nil { t.Fatal("reopening diagnostics failed", err) } } func TestDiagnosticWriteFailureDoesNotReplacePlaybackError(t *testing.T) { directory := t.TempDir() if err := os.WriteFile(filepath.Join(directory, "logs"), []byte("keep"), 0600); err != nil { t.Fatal(err) } log := newDiagnosticLog(directory) if err := log.write(diagnosticEvent{Event: "fixture.failed", Message: "failure"}); err == nil { t.Fatal("unwritable diagnostics unexpectedly succeeded") } d := &Downloader{diagnostics: log} d.recordTaskFailure("playback.failed", Task{}, 49, errors.New("unexpected EOF")) data, err := os.ReadFile(filepath.Join(directory, "logs")) if err != nil || string(data) != "keep" { t.Fatal("diagnostics replaced an existing file", err) } } func TestPlaybackFailureLogIncludesEpisodeAndSkipsStoppedRun(t *testing.T) { app, session := prefetchFixtureApp(t) session.tasks[0] = Task{DramaID: "hongguo:7000000000000000001", DramaTitle: "合成播放条目", Index: 1} run, _, err := app.beginPlayback(viewerFixtureContext(app, context.Background()), "fixture", 1, 49, 0, 1, false) if err != nil { t.Fatal(err) } defer run.stop() app.finishPlaybackRun(run, errors.New("unexpected EOF"), false) app.finishPlaybackRun(run, context.Canceled, true) events := readDiagnosticEvents(t, app.downloader.diagnostics.path) failures := 0 for _, event := range events { if event.Event == "playback.failed" { failures++ if event.DramaID != run.task.DramaID || event.DramaTitle != run.task.DramaTitle || event.Episode != 1 || event.StartSeconds != 49 || event.Message != "unexpected EOF" { t.Fatal("playback failure lost its diagnostic context", fmt.Sprint(event)) } } } if failures != 1 { t.Fatal("stopping playback generated a false diagnostic failure") } }