Files
Havline/internal/terminal/recording_test.go
T
mrlee 302248a907
CI / verify (push) Canceled after 0s
chore(release): 发布 v2.0.0
- 内置 SSH 终端(/terminal):复用已保存的 SSH 密码/私钥与独立 known_hosts 校验
- 终端二次认证、会话审计(/terminal/audits)、录像审计与常用命令
- DDNS 服务商名称与自定义 IP,新增迁移 037/038/039
- CHANGELOG 与 VERSION 更新至 2.0.0
2026-10-16 20:18:04 +08:00

169 lines
4.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package terminal
import (
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
)
// fakeRecorder 收集写进来的输出,用于验证会话是否把输出接到录制器上。
type fakeRecorder struct {
mu sync.Mutex
buf strings.Builder
closed bool
}
func (r *fakeRecorder) Write(p []byte) {
r.mu.Lock()
defer r.mu.Unlock()
r.buf.Write(p)
}
func (r *fakeRecorder) Close() {
r.mu.Lock()
defer r.mu.Unlock()
r.closed = true
}
func (r *fakeRecorder) text() string {
r.mu.Lock()
defer r.mu.Unlock()
return r.buf.String()
}
func (r *fakeRecorder) isClosed() bool {
r.mu.Lock()
defer r.mu.Unlock()
return r.closed
}
// TestCastRecorderAndStore:录制写出合法的 asciinema v2 文件,列表能读到元信息,非法 ID 被拒。
func TestCastRecorderAndStore(t *testing.T) {
dir := t.TempDir()
store := NewRecordingStore(dir)
started := time.Now().Add(-3 * time.Second)
recorder, err := NewCastRecorder(store, "abcd1234", "服务端 #1", 120, 40, started)
if err != nil {
t.Fatalf("创建录制失败:%v", err)
}
recorder.Write([]byte("hello "))
// 无效 UTF-8 必须被替换,否则会写出非法 JSON
recorder.Write([]byte{0xff, 0xfe})
recorder.Write([]byte("world\r\n"))
recorder.Close()
list, err := store.List()
if err != nil {
t.Fatalf("列表失败:%v", err)
}
if len(list) != 1 {
t.Fatalf("期望 1 条录制,实际 %d 条", len(list))
}
if list[0].SessionID != "abcd1234" || list[0].Size == 0 || list[0].StartedAt == "" || list[0].EndedAt == "" {
t.Fatalf("录制元信息不完整:%+v", list[0])
}
raw, err := os.ReadFile(filepath.Join(dir, "abcd1234.cast"))
if err != nil {
t.Fatalf("读取录制失败:%v", err)
}
lines := strings.Split(strings.TrimSpace(string(raw)), "\n")
if len(lines) != 4 {
t.Fatalf("期望 4 行(头 + 3 帧),实际 %d 行", len(lines))
}
var header struct {
Version int `json:"version"`
Width int `json:"width"`
Height int `json:"height"`
}
if err := json.Unmarshal([]byte(lines[0]), &header); err != nil || header.Version != 2 {
t.Fatalf("cast 头不合法:%s", lines[0])
}
if header.Width != 120 || header.Height != 40 {
t.Fatalf("cast 头宽高不符:%+v", header)
}
var frame []any
if err := json.Unmarshal([]byte(lines[1]), &frame); err != nil {
t.Fatalf("帧不是合法 JSON:%s", lines[1])
}
if len(frame) != 3 || frame[1] != "o" {
t.Fatalf("帧结构不符:%+v", frame)
}
// 路径穿越与畸形 ID 必须被拒
for _, id := range []string{"../etc/passwd", "ab", "a/b", ""} {
if _, ok := store.Path(id); ok {
t.Fatalf("非法会话 ID %q 不应通过校验", id)
}
}
// 过期录制会被清理
stale := filepath.Join(dir, "stale1234.cast")
if err := os.WriteFile(stale, []byte("{}\n"), 0o644); err != nil {
t.Fatalf("准备过期录制失败:%v", err)
}
past := time.Now().Add(-48 * time.Hour)
if err := os.Chtimes(stale, past, past); err != nil {
t.Fatalf("调整时间失败:%v", err)
}
removed, err := store.Prune(time.Now().Add(-24 * time.Hour))
if err != nil || removed != 1 {
t.Fatalf("期望清理 1 条,实际 %d 条 err=%v", removed, err)
}
}
// TestManagerRecordsOutput:开启录制时输出会流进录制器,会话结束时录制器被关闭。
func TestManagerRecordsOutput(t *testing.T) {
open, created := openWith()
manager := NewManager(testLogger(), open, Options{})
recorder := &fakeRecorder{}
manager.SetRecorderFactory(func(string, CreateOptions) (OutputRecorder, error) {
return recorder, nil
})
opts := testCreateOptions(1)
opts.Record = true
if _, err := manager.Create(context.Background(), opts); err != nil {
t.Fatalf("创建会话失败:%v", err)
}
if _, err := (*created)[0].writer.Write([]byte("record-me")); err != nil {
t.Fatalf("向后端写入失败:%v", err)
}
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) && !strings.Contains(recorder.text(), "record-me") {
time.Sleep(10 * time.Millisecond)
}
if !strings.Contains(recorder.text(), "record-me") {
t.Fatalf("输出没有进入录制器:%q", recorder.text())
}
manager.CloseAll("测试结束")
if !recorder.isClosed() {
t.Fatal("会话结束后录制器应当被关闭")
}
}
// TestManagerSkipsRecordingWhenOff:未开启录制时不创建录制器。
func TestManagerSkipsRecordingWhenOff(t *testing.T) {
open, _ := openWith()
manager := NewManager(testLogger(), open, Options{})
called := false
manager.SetRecorderFactory(func(string, CreateOptions) (OutputRecorder, error) {
called = true
return &fakeRecorder{}, nil
})
if _, err := manager.Create(context.Background(), testCreateOptions(1)); err != nil {
t.Fatalf("创建会话失败:%v", err)
}
manager.CloseAll("测试结束")
if called {
t.Fatal("未开启录制时不应创建录制器")
}
}