Files
90CNServer/server/internal/platform/logcrypt/logcrypt_test.go
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

479 lines
14 KiB
Go

package logcrypt
import (
"bytes"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// testIdentity 生成一对测试密钥,并返回收件人与私钥。
func testIdentity(t *testing.T) (*Recipient, []byte) {
t.Helper()
publicKey, privateKey, err := GenerateKeyPair()
if err != nil {
t.Fatalf("generate key pair: %v", err)
}
recipient, err := ParseRecipientPublicKey(publicKey)
if err != nil {
t.Fatalf("parse public key: %v", err)
}
return recipient, privateKey
}
func TestKeyPairRoundTripAndEncoding(t *testing.T) {
recipient, privateKey := testIdentity(t)
if recipient.KeyID() == "" || len(recipient.KeyID()) != 16 {
t.Fatalf("key id = %q", recipient.KeyID())
}
// 公钥接受 base64 / hex / 原始字节
publicBytes := recipient.PublicKeyBytes()
for name, payload := range map[string][]byte{
"raw": publicBytes,
"base64": EncodeKeyFile(publicBytes),
} {
parsed, err := ParseRecipientPublicKey(payload)
if err != nil {
t.Fatalf("%s: parse public key: %v", name, err)
}
if parsed.KeyID() != recipient.KeyID() {
t.Fatalf("%s: key id mismatch", name)
}
}
if _, err := ParseRecipientPublicKey([]byte("too-short")); !errors.Is(err, ErrKeyInvalid) {
t.Fatalf("short public key error = %v, want ErrKeyInvalid", err)
}
if _, err := ParsePrivateKey(EncodeKeyFile(privateKey)); err != nil {
t.Fatalf("parse private key: %v", err)
}
if _, err := ParsePrivateKey([]byte("nope")); !errors.Is(err, ErrKeyInvalid) {
t.Fatalf("short private key error = %v, want ErrKeyInvalid", err)
}
}
// 契约:**没有收件人公钥就不许写**——绝不退化成明文。
func TestOpenWithoutRecipientIsRefused(t *testing.T) {
if _, err := Open(Config{Dir: t.TempDir(), Prefix: "packet", Ext: ".log.enc"}); !errors.Is(err, ErrRecipientMissing) {
t.Fatalf("open without recipient = %v, want ErrRecipientMissing", err)
}
if _, err := NewEncoder(nil, &bytes.Buffer{}, nil); !errors.Is(err, ErrRecipientMissing) {
t.Fatalf("encoder without recipient = %v, want ErrRecipientMissing", err)
}
if _, err := NewStreamWriter(nil, &bytes.Buffer{}, nil); !errors.Is(err, ErrRecipientMissing) {
t.Fatalf("stream writer without recipient = %v, want ErrRecipientMissing", err)
}
}
func TestWriteKeyPairRefusesToOverwrite(t *testing.T) {
dir := t.TempDir()
publicPath, privatePath, keyID, err := WriteKeyPair(dir, false)
if err != nil {
t.Fatalf("write key pair: %v", err)
}
if keyID == "" {
t.Fatal("empty key id")
}
if _, _, _, err := WriteKeyPair(dir, false); err == nil {
t.Fatal("second WriteKeyPair without force must refuse")
}
if _, _, _, err := WriteKeyPair(dir, true); err != nil {
t.Fatalf("forced rewrite: %v", err)
}
for _, path := range []string{publicPath, privatePath} {
info, statErr := os.Stat(path)
if statErr != nil {
t.Fatalf("stat %s: %v", path, statErr)
}
if info.Size() == 0 {
t.Fatalf("%s is empty", path)
}
}
}
// 核心契约:磁盘上不得出现明文,且用私钥解密后与写入内容逐字节一致。
func TestWriterNeverWritesPlaintextAndRoundTrips(t *testing.T) {
dir := t.TempDir()
recipient, privateKey := testIdentity(t)
writer, err := Open(Config{Dir: dir, Prefix: "packet", Ext: ".log.enc", Recipient: recipient})
if err != nil {
t.Fatalf("open writer: %v", err)
}
secret := "SEND kind=game-upper conn_id=game-000001 hex=DEADBEEFCAFE"
writer.Write("EVENT kind=starts " + secret)
writer.Write("EVENT kind=second index=1")
if err := writer.Close(); err != nil {
t.Fatalf("close: %v", err)
}
path := writer.Path()
if path == "" {
path = CurrentPath(dir)
}
if path == "" {
t.Fatal("no current file path recorded")
}
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read container: %v", err)
}
for _, needle := range []string{"DEADBEEF", "conn_id", "game-upper", "EVENT kind"} {
if bytes.Contains(raw, []byte(needle)) {
t.Fatalf("plaintext %q found on disk in %s", needle, path)
}
}
if !IsContainer(raw) {
t.Fatal("written file is not a logcrypt container")
}
if keyID, err := ContainerKeyID(raw); err != nil || keyID != recipient.KeyID() {
t.Fatalf("container key id = %q err=%v want %q", keyID, err, recipient.KeyID())
}
plain, records, truncated, err := DecryptAll(privateKey, raw)
if err != nil {
t.Fatalf("decrypt: %v", err)
}
if truncated || records != 2 {
t.Fatalf("records=%d truncated=%t", records, truncated)
}
text := string(plain)
if !strings.Contains(text, secret) || !strings.Contains(text, "EVENT kind=second index=1") {
t.Fatalf("round-trip mismatch:\n%s", text)
}
}
// 只有公钥的人(= 拿到服务端包的人)解不开。
func TestPublicKeyAloneCannotDecrypt(t *testing.T) {
dir := t.TempDir()
recipient, _ := testIdentity(t)
writer, err := Open(Config{Dir: dir, Prefix: "packet", Ext: ".log.enc", Recipient: recipient})
if err != nil {
t.Fatalf("open: %v", err)
}
writer.Write("EVENT kind=secret")
if err := writer.Close(); err != nil {
t.Fatalf("close: %v", err)
}
raw, err := os.ReadFile(writer.Path())
if err != nil {
t.Fatalf("read: %v", err)
}
// 用另一对密钥的私钥必须失败。
_, otherPrivate := testIdentity(t)
if _, _, _, err := DecryptAll(otherPrivate, raw); err == nil {
t.Fatal("decrypt with an unrelated private key succeeded")
}
// 公钥本身不是私钥,也必须失败。
if _, _, _, err := DecryptAll(recipient.PublicKeyBytes(), raw); err == nil {
t.Fatal("decrypt using the public key as private key succeeded")
}
}
func TestWriterRotatesBySizeAndKeepsEveryFileReadable(t *testing.T) {
dir := t.TempDir()
recipient, privateKey := testIdentity(t)
writer, err := Open(Config{
Dir: dir, Prefix: "packet", Ext: ".log.enc", Recipient: recipient,
MaxBytes: 4096,
})
if err != nil {
t.Fatalf("open: %v", err)
}
const lines = 300
for index := 0; index < lines; index++ {
writer.Write(strings.Repeat("x", 64) + " index=" + itoa(index))
}
if err := writer.Close(); err != nil {
t.Fatalf("close: %v", err)
}
files, err := OpenFiles(dir, "packet")
if err != nil {
t.Fatalf("list: %v", err)
}
if len(files) < 2 {
t.Fatalf("expected rotation into multiple files, got %d", len(files))
}
total := 0
for _, file := range files {
raw, readErr := os.ReadFile(file)
if readErr != nil {
t.Fatalf("read %s: %v", file, readErr)
}
if int64(len(raw)) > 4096+8192 {
t.Fatalf("%s exceeded the cap badly: %d bytes", file, len(raw))
}
plain, records, truncated, decErr := DecryptAll(privateKey, raw)
if decErr != nil || truncated {
t.Fatalf("decrypt %s: records=%d truncated=%t err=%v", file, records, truncated, decErr)
}
if records > 0 && !strings.Contains(string(plain), "index=") {
t.Fatalf("%s decrypted without the expected record body", file)
}
total += records
}
if total != lines {
t.Fatalf("records across files = %d, want %d", total, lines)
}
}
func TestWriterRotatesOnDayBoundaryAndPrunesOldDirs(t *testing.T) {
dir := t.TempDir()
recipient, privateKey := testIdentity(t)
day := time.Date(2026, 3, 1, 12, 0, 0, 0, time.UTC)
now := day
writer, err := Open(Config{
Dir: dir, Prefix: "packet", Ext: ".log.enc", Recipient: recipient,
RetentionDays: 7,
Now: func() time.Time { return now },
})
if err != nil {
t.Fatalf("open: %v", err)
}
writer.Write("EVENT kind=day-one")
waitForDir(t, filepath.Join(dir, "20260301"), true)
now = day.AddDate(0, 0, 1) // 20260302,保留窗口从 20260223 起
writer.Write("EVENT kind=day-two")
waitForDir(t, filepath.Join(dir, "20260302"), true)
if _, statErr := os.Stat(filepath.Join(dir, "20260301")); statErr != nil {
t.Fatalf("20260301 must survive while inside the retention window: %v", statErr)
}
now = day.AddDate(0, 0, 9) // 20260310,截止 20260303
writer.Write("EVENT kind=day-nine")
if err := writer.Close(); err != nil {
t.Fatalf("close: %v", err)
}
waitForDir(t, filepath.Join(dir, "20260310"), true)
files, err := OpenFiles(dir, "packet")
if err != nil {
t.Fatalf("list: %v", err)
}
found := false
for _, file := range files {
if !strings.Contains(file, "20260310") {
continue
}
found = true
raw, readErr := os.ReadFile(file)
if readErr != nil {
t.Fatalf("read %s: %v", file, readErr)
}
plain, _, truncated, decErr := DecryptAll(privateKey, raw)
if decErr != nil || truncated || !strings.Contains(string(plain), "EVENT kind=day-nine") {
t.Fatalf("decrypt %s: truncated=%t err=%v", file, truncated, decErr)
}
}
if !found {
t.Fatal("no container written for the current day")
}
waitForDir(t, filepath.Join(dir, "20260301"), false)
waitForDir(t, filepath.Join(dir, "20260302"), false)
}
func waitForDir(t *testing.T, path string, want bool) {
t.Helper()
deadline := time.Now().Add(3 * time.Second)
for {
_, err := os.Stat(path)
exists := err == nil
if exists == want {
return
}
if time.Now().After(deadline) {
if want {
t.Fatalf("directory %s never appeared: %v", path, err)
}
t.Fatalf("directory %s was not pruned", path)
}
time.Sleep(5 * time.Millisecond)
}
}
func TestDecryptAllToleratesTruncatedTail(t *testing.T) {
dir := t.TempDir()
recipient, privateKey := testIdentity(t)
writer, err := Open(Config{Dir: dir, Prefix: "packet", Ext: ".log.enc", Recipient: recipient})
if err != nil {
t.Fatalf("open: %v", err)
}
writer.Write("EVENT kind=first")
writer.Write("EVENT kind=second")
if err := writer.Close(); err != nil {
t.Fatalf("close: %v", err)
}
raw, err := os.ReadFile(writer.Path())
if err != nil {
t.Fatalf("read: %v", err)
}
cut := raw[:len(raw)-9] // 砍掉最后一条的尾巴,模拟掉电半行
plain, records, truncated, err := DecryptAll(privateKey, cut)
if err != nil {
t.Fatalf("decrypt truncated: %v", err)
}
if !truncated || records != 1 || !strings.Contains(string(plain), "EVENT kind=first") {
t.Fatalf("records=%d truncated=%t plain=%q", records, truncated, string(plain))
}
}
func TestCurrentPointerFollowsRotation(t *testing.T) {
dir := t.TempDir()
recipient, _ := testIdentity(t)
writer, err := Open(Config{Dir: dir, Prefix: "packet", Ext: ".log.enc", Recipient: recipient, MaxBytes: 2048})
if err != nil {
t.Fatalf("open: %v", err)
}
for index := 0; index < 100; index++ {
writer.Write(strings.Repeat("y", 96) + " i=" + itoa(index))
}
if err := writer.Close(); err != nil {
t.Fatalf("close: %v", err)
}
pointer := CurrentPath(dir)
if pointer == "" {
t.Fatal("current.txt missing")
}
if _, err := os.Stat(pointer); err != nil {
t.Fatalf("pointer target missing: %v", err)
}
if filepath.Base(pointer) != filepath.Base(writer.Path()) {
t.Fatalf("pointer target %q != last written %q", pointer, writer.Path())
}
}
// gatedReader 在 release 关闭前阻塞,用来确定性地制造写队列积压。
type gatedReader struct {
release chan struct{}
}
func (g *gatedReader) Read(p []byte) (int, error) {
<-g.release
return defaultRandom().Read(p)
}
func TestDropMarkerIsEncryptedToo(t *testing.T) {
dir := t.TempDir()
recipient, privateKey := testIdentity(t)
gate := &gatedReader{release: make(chan struct{})}
writer, err := Open(Config{
Dir: dir, Prefix: "packet", Ext: ".log.enc", Recipient: recipient,
QueueSize: 1,
DropMarkerKind: "packet-log-backpressure-dropped",
Random: gate,
})
if err != nil {
t.Fatalf("open: %v", err)
}
for index := 0; index < 200; index++ {
writer.Write("EVENT kind=flood index=" + itoa(index))
}
if writer.Dropped() == 0 {
t.Fatal("expected the bounded queue to drop under a stalled writer")
}
close(gate.release)
writer.Write("EVENT kind=after-release")
if err := writer.Close(); err != nil {
t.Fatalf("close: %v", err)
}
raw, err := os.ReadFile(writer.Path())
if err != nil {
t.Fatalf("read: %v", err)
}
if bytes.Contains(raw, []byte("backpressure-dropped")) {
t.Fatal("drop marker leaked to disk in plaintext")
}
plain, _, _, err := DecryptAll(privateKey, raw)
if err != nil {
t.Fatalf("decrypt: %v", err)
}
if !strings.Contains(string(plain), "EVENT kind=packet-log-backpressure-dropped") {
t.Fatal("drop marker missing from the decrypted stream")
}
}
// StreamWriter 用于 os/exec 的 stdout/stderr:行边界可能被切开,必须按行重组。
func TestStreamWriterSplitsPartialLines(t *testing.T) {
dir := t.TempDir()
recipient, privateKey := testIdentity(t)
path := filepath.Join(dir, "stream.log.enc")
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
if err != nil {
t.Fatalf("create: %v", err)
}
stream, err := NewStreamWriter(recipient, file, nil)
if err != nil {
t.Fatalf("new stream writer: %v", err)
}
// 故意在一行中间切开
if _, err := stream.Write([]byte("hello wo")); err != nil {
t.Fatalf("write 1: %v", err)
}
if _, err := stream.Write([]byte("rld\nsecond line\nthird")); err != nil {
t.Fatalf("write 2: %v", err)
}
if err := stream.Close(); err != nil {
t.Fatalf("close: %v", err)
}
if err := file.Close(); err != nil {
t.Fatalf("close file: %v", err)
}
plain, records, _, err := ReadFilePlaintext(privateKey, path)
if err != nil {
t.Fatalf("read back: %v", err)
}
// 三条记录:完整的两行 + Close 时落下的残行
if records != 3 {
t.Fatalf("records = %d, want 3 (plain=%q)", records, string(plain))
}
text := string(plain)
// 末尾残行原样保留(不加换行):解开后的拼接必须与子进程写出的字节流一致。
if text != "hello world\nsecond line\nthird" {
t.Fatalf("stream text = %q", text)
}
for _, want := range []string{"hello world\n", "second line\n", "third"} {
if !strings.Contains(text, want) {
t.Fatalf("missing %q in %q", want, text)
}
}
}
func TestOptionalPrivateKeyMissingIsNotAnError(t *testing.T) {
key, err := OptionalPrivateKey(filepath.Join(t.TempDir(), "absent.key"))
if err != nil || key != nil {
t.Fatalf("missing optional key = (%v, %v), want (nil, nil)", key, err)
}
key, err = OptionalPrivateKey("")
if err != nil || key != nil {
t.Fatalf("empty optional key = (%v, %v), want (nil, nil)", key, err)
}
}
func TestReadFilePlaintextRejectsNonContainer(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "plain.txt")
if err := os.WriteFile(path, []byte("not encrypted\n"), 0o600); err != nil {
t.Fatalf("write: %v", err)
}
_, privateKey := testIdentity(t)
if _, _, _, err := ReadFilePlaintext(privateKey, path); !errors.Is(err, ErrNotEncrypted) {
t.Fatalf("err = %v, want ErrNotEncrypted", err)
}
}
func itoa(value int) string {
if value == 0 {
return "0"
}
var buf [20]byte
pos := len(buf)
for value > 0 {
pos--
buf[pos] = byte('0' + value%10)
value /= 10
}
return string(buf[pos:])
}