150 lines
4.1 KiB
Go
150 lines
4.1 KiB
Go
package gopop
|
||
|
||
import (
|
||
"bufio"
|
||
"errors"
|
||
"net"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// 仅替换数据提供层,命令解析和响应编码执行真实实现;不启动公网监听。
|
||
type auditPOP3Action struct {
|
||
securityTestAction
|
||
committed []int64
|
||
topLines []int
|
||
}
|
||
|
||
func (a *auditPOP3Action) Delete(session *Session, id int64) error {
|
||
session.DeleteIds = append(session.DeleteIds, id)
|
||
return nil
|
||
}
|
||
|
||
func (a *auditPOP3Action) Rest(session *Session) error {
|
||
session.DeleteIds = nil
|
||
return nil
|
||
}
|
||
|
||
func (a *auditPOP3Action) Quit(session *Session) error {
|
||
a.committed = append([]int64(nil), session.DeleteIds...)
|
||
return nil
|
||
}
|
||
|
||
func (a *auditPOP3Action) Custom(session *Session, cmd string, args []string) ([]string, error) {
|
||
return nil, errors.New("unsupported command")
|
||
}
|
||
|
||
func (a *auditPOP3Action) List(session *Session, msg string) ([]MailInfo, error) {
|
||
return []MailInfo{{Id: 1, Size: 100}}, nil
|
||
}
|
||
|
||
func (a *auditPOP3Action) Uidl(session *Session, msg string) ([]UidlItem, error) {
|
||
return []UidlItem{{Id: 1, UnionId: "audit-message"}}, nil
|
||
}
|
||
|
||
func (a *auditPOP3Action) Top(session *Session, id int64, n int) (string, error) {
|
||
a.topLines = append(a.topLines, n)
|
||
return "fixture", nil
|
||
}
|
||
|
||
func auditPOP3Pipe(t *testing.T, action Action) (net.Conn, *bufio.Reader, <-chan struct{}) {
|
||
t.Helper()
|
||
serverConn, clientConn := net.Pipe()
|
||
server := &Server{Domain: "audit.invalid", Action: action}
|
||
done := make(chan struct{})
|
||
go func() {
|
||
server.handleClient(serverConn)
|
||
close(done)
|
||
}()
|
||
t.Cleanup(func() {
|
||
_ = clientConn.Close()
|
||
_ = serverConn.Close()
|
||
select {
|
||
case <-done:
|
||
case <-time.After(2 * time.Second):
|
||
t.Error("隔离 POP3 解析器未及时退出")
|
||
}
|
||
})
|
||
if err := clientConn.SetDeadline(time.Now().Add(2 * time.Second)); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
reader := bufio.NewReader(clientConn)
|
||
readResponseLine(t, reader, "+OK")
|
||
writeCommand(t, clientConn, "USER audit")
|
||
readResponseLine(t, reader, "+OK")
|
||
writeCommand(t, clientConn, "PASS fixture-only")
|
||
readResponseLine(t, reader, "+OK")
|
||
return clientConn, reader, done
|
||
}
|
||
|
||
func TestAuditPOP3RSETMustCancelPendingDeletion(t *testing.T) {
|
||
action := &auditPOP3Action{}
|
||
conn, reader, done := auditPOP3Pipe(t, action)
|
||
writeCommand(t, conn, "DELE 1")
|
||
readResponseLine(t, reader, "+OK")
|
||
writeCommand(t, conn, "RSET")
|
||
response, err := reader.ReadString('\n')
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
writeCommand(t, conn, "QUIT")
|
||
readResponseLine(t, reader, "+OK")
|
||
<-done
|
||
t.Logf("rset_response=%q committed_ids=%v", strings.TrimSpace(response), action.committed)
|
||
if !strings.HasPrefix(response, "+OK") || len(action.committed) != 0 {
|
||
t.Fatal("标准 RSET 没有撤销删除标记,QUIT 仍提交原删除集合")
|
||
}
|
||
}
|
||
|
||
func TestAuditPOP3SingleMessageRepliesMustBeSingleLine(t *testing.T) {
|
||
for _, test := range []struct {
|
||
command string
|
||
want string
|
||
}{
|
||
{command: "LIST 1", want: "+OK 1 100"},
|
||
{command: "UIDL 1", want: "+OK 1 audit-message"},
|
||
} {
|
||
t.Run(test.command, func(t *testing.T) {
|
||
conn, reader, _ := auditPOP3Pipe(t, &auditPOP3Action{})
|
||
writeCommand(t, conn, test.command)
|
||
response, err := reader.ReadString('\n')
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
t.Logf("command=%q first_response_line=%q buffered_extra_bytes=%d", test.command, strings.TrimSpace(response), reader.Buffered())
|
||
if strings.TrimSpace(response) != test.want || reader.Buffered() != 0 {
|
||
t.Fatal("指定消息编号的 LIST/UIDL 被错误编码成多行响应")
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestAuditPOP3ParserMustRejectNegativeTOPLines(t *testing.T) {
|
||
action := &auditPOP3Action{}
|
||
conn, reader, done := auditPOP3Pipe(t, action)
|
||
writeCommand(t, conn, "TOP 1 -100000")
|
||
response, err := reader.ReadString('\n')
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if strings.HasPrefix(response, "+OK") {
|
||
for {
|
||
line, err := reader.ReadString('\n')
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if line == ".\r\n" {
|
||
break
|
||
}
|
||
}
|
||
}
|
||
writeCommand(t, conn, "QUIT")
|
||
readResponseLine(t, reader, "+OK")
|
||
<-done
|
||
t.Logf("negative_top_response=%q dispatched_line_counts=%v", strings.TrimSpace(response), action.topLines)
|
||
if !strings.HasPrefix(response, "-ERR") || len(action.topLines) != 0 {
|
||
t.Fatal("TOP 的负数行参数未在协议入口拒绝")
|
||
}
|
||
}
|