We build too many walls and not enough bridges

This commit is contained in:
Jakub
2020-04-08 12:59:16 +02:00
commit 17f4d6097a
494 changed files with 62753 additions and 0 deletions

78
test/mocks/debug.go Normal file
View File

@ -0,0 +1,78 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package mocks
import (
"fmt"
"os"
"strings"
"time"
"github.com/logrusorgru/aurora"
)
type debug struct {
verbosity int
reqTag string
}
func newDebug(reqTag string) *debug {
return &debug{
verbosity: verbosityLevelFromEnv(),
reqTag: reqTag,
}
}
func verbosityLevelFromEnv() int {
verbosityName := os.Getenv("VERBOSITY")
switch strings.ToLower(verbosityName) {
case "error", "fatal", "panic":
return 0
case "warning":
return 1
case "info":
return 2
case "debug", "trace":
return 3
}
return 2
}
func (d *debug) printReq(command string) {
if d.verbosity > 0 {
fmt.Println(aurora.Green(fmt.Sprintf("Req %s: %s", d.reqTag, command)))
}
}
func (d *debug) printRes(line string) {
if d.verbosity > 1 {
line = strings.ReplaceAll(line, "\n", "")
line = strings.ReplaceAll(line, "\r", "")
fmt.Println(aurora.Cyan(fmt.Sprintf("Res %s: %s", d.reqTag, line)))
}
}
func (d *debug) printErr(line string) {
fmt.Print(aurora.Bold(aurora.Red(fmt.Sprintf("Res %s: %s", d.reqTag, line))))
}
func (d *debug) printTime(diff time.Duration) {
if d.verbosity > 0 {
fmt.Println(aurora.Green(fmt.Sprintf("Time %s:%v", d.reqTag, diff)))
}
}

227
test/mocks/imap.go Normal file
View File

@ -0,0 +1,227 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package mocks
import (
"bufio"
"fmt"
"net"
"sync"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type IMAPClient struct {
lock *sync.Mutex
debug *debug
t TestingT
reqTag string
reqIndex int // global request index for this client
conn net.Conn
response *bufio.Reader
idling bool
}
func NewIMAPClient(t TestingT, tag string, imapAddr string) *IMAPClient {
conn, err := net.Dial("tcp", imapAddr)
require.NoError(t, err)
response := bufio.NewReader(conn)
// Read first response to opening connection.
_, err = response.ReadString('\n')
assert.NoError(t, err)
return &IMAPClient{
lock: &sync.Mutex{},
debug: newDebug(tag),
t: t,
reqTag: tag,
reqIndex: 0,
conn: conn,
response: response,
}
}
func (c *IMAPClient) Close() {
c.lock.Lock()
defer c.lock.Unlock()
if c.idling {
c.StopIDLE()
}
_ = c.conn.Close()
}
func (c *IMAPClient) SendCommand(command string) *IMAPResponse {
c.lock.Lock()
defer c.lock.Unlock()
imapResponse := &IMAPResponse{t: c.t}
go imapResponse.sendCommand(c.reqTag, c.reqIndex, command, c.debug, c.conn, c.response)
c.reqIndex++
return imapResponse
}
// Auth
func (c *IMAPClient) Login(account, password string) *IMAPResponse {
return c.SendCommand(fmt.Sprintf("LOGIN %s %s", account, password))
}
func (c *IMAPClient) Logout() *IMAPResponse {
return c.SendCommand("LOGOUT")
}
// Mailboxes
func (c *IMAPClient) ListMailboxes() *IMAPResponse {
return c.SendCommand("LIST \"\" *")
}
func (c *IMAPClient) Select(mailboxName string) *IMAPResponse {
return c.SendCommand(fmt.Sprintf("SELECT \"%s\"", mailboxName)) //nolint[gosec]
}
func (c *IMAPClient) CreateMailbox(mailboxName string) *IMAPResponse {
return c.SendCommand(fmt.Sprintf("CREATE \"%s\"", mailboxName))
}
func (c *IMAPClient) DeleteMailbox(mailboxName string) *IMAPResponse {
return c.SendCommand(fmt.Sprintf("DELETE \"%s\"", mailboxName)) //nolint[gosec]
}
func (c *IMAPClient) RenameMailbox(mailboxName, newMailboxName string) *IMAPResponse {
return c.SendCommand(fmt.Sprintf("RENAME \"%s\" \"%s\"", mailboxName, newMailboxName))
}
func (c *IMAPClient) GetMailboxInfo(mailboxName string) *IMAPResponse {
return c.SendCommand(fmt.Sprintf("EXAMINE \"%s\"", mailboxName))
}
func (c *IMAPClient) GetMailboxStatus(mailboxName string) *IMAPResponse {
return c.SendCommand(fmt.Sprintf("STATUS \"%s\" (MESSAGES UNSEEN UIDNEXT UIDVALIDITY)", mailboxName))
}
// Messages
func (c *IMAPClient) FetchAllFlags() *IMAPResponse {
return c.FetchAll("flags")
}
func (c *IMAPClient) FetchAllSubjects() *IMAPResponse {
return c.FetchAll("body.peek[header.fields (subject)]")
}
func (c *IMAPClient) FetchAllHeaders() *IMAPResponse {
return c.FetchAll("body.peek[header]")
}
func (c *IMAPClient) FetchAllBodyStructures() *IMAPResponse {
return c.FetchAll("bodystructure")
}
func (c *IMAPClient) FetchAllSizes() *IMAPResponse {
return c.FetchAll("rfc822.size")
}
func (c *IMAPClient) FetchAllBodies() *IMAPResponse {
return c.FetchAll("rfc822")
}
func (c *IMAPClient) FetchAll(parts string) *IMAPResponse {
return c.Fetch("1:*", parts)
}
func (c *IMAPClient) Fetch(ids, parts string) *IMAPResponse {
return c.SendCommand(fmt.Sprintf("FETCH %s %s", ids, parts))
}
func (c *IMAPClient) FetchUID(ids, parts string) *IMAPResponse {
return c.SendCommand(fmt.Sprintf("UID FETCH %s %s", ids, parts))
}
func (c *IMAPClient) Search(query string) *IMAPResponse {
return c.SendCommand(fmt.Sprintf("SEARCH %s", query))
}
// Message
func (c *IMAPClient) Append(mailboxName, subject, from, to, body string) *IMAPResponse {
msg := fmt.Sprintf("Subject: %s\r\n", subject)
msg += fmt.Sprintf("From: %s\r\n", from)
msg += fmt.Sprintf("To: %s\r\n", to)
msg += "\r\n"
msg += body
msg += "\r\n"
cmd := fmt.Sprintf("APPEND \"%s\" (\\Seen) \"25-Mar-2021 00:30:00 +0100\" {%d}\r\n%s", mailboxName, len(msg), msg)
return c.SendCommand(cmd)
}
func (c *IMAPClient) Delete(ids string) *IMAPResponse {
return c.AddFlags(ids, "\\Deleted")
}
func (c *IMAPClient) Copy(ids, newMailboxName string) *IMAPResponse {
return c.SendCommand(fmt.Sprintf("COPY %s \"%s\"", ids, newMailboxName))
}
func (c *IMAPClient) Move(ids, newMailboxName string) *IMAPResponse {
return c.SendCommand(fmt.Sprintf("MOVE %s \"%s\"", ids, newMailboxName))
}
func (c *IMAPClient) MarkAsRead(ids string) *IMAPResponse {
return c.AddFlags(ids, "\\Seen")
}
func (c *IMAPClient) MarkAsUnread(ids string) *IMAPResponse {
return c.RemoveFlags(ids, "\\Seen")
}
func (c *IMAPClient) MarkAsStarred(ids string) *IMAPResponse {
return c.AddFlags(ids, "\\Flagged")
}
func (c *IMAPClient) MarkAsUnstarred(ids string) *IMAPResponse {
return c.RemoveFlags(ids, "\\Flagged")
}
func (c *IMAPClient) AddFlags(ids, flags string) *IMAPResponse {
return c.changeFlags(ids, flags, "+")
}
func (c *IMAPClient) RemoveFlags(ids, flags string) *IMAPResponse {
return c.changeFlags(ids, flags, "-")
}
func (c *IMAPClient) changeFlags(ids, flags, op string) *IMAPResponse {
return c.SendCommand(fmt.Sprintf("STORE %s %sflags (%s)", ids, op, flags))
}
// IDLE
func (c *IMAPClient) StartIDLE() *IMAPResponse {
c.idling = true
return c.SendCommand("IDLE")
}
func (c *IMAPClient) StopIDLE() {
c.idling = false
fmt.Fprintf(c.conn, "%s\r\n", "DONE")
}

199
test/mocks/imap_response.go Normal file
View File

@ -0,0 +1,199 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package mocks
import (
"bufio"
"fmt"
"io"
"regexp"
"strings"
"time"
"github.com/pkg/errors"
a "github.com/stretchr/testify/assert"
)
type IMAPResponse struct {
t TestingT
err error
result string
sections []string
done bool
}
func (ir *IMAPResponse) sendCommand(reqTag string, reqIndex int, command string, debug *debug, conn io.Writer, response *bufio.Reader) {
defer func() { ir.done = true }()
tstart := time.Now()
commandID := fmt.Sprintf("%sO%0d", reqTag, reqIndex)
command = fmt.Sprintf("%s %s", commandID, command)
debug.printReq(command)
fmt.Fprintf(conn, "%s\r\n", command)
var section string
for {
line, err := response.ReadString('\n')
if err != nil {
ir.err = errors.Wrap(err, "read response failed")
debug.printErr(ir.err.Error() + "\n")
return
}
// Finishing line contains `commandID` following with status (`NO`, `BAD`, ...) and then message itself.
lineWithoutID := strings.Replace(line, commandID+" ", "", 1)
if strings.HasPrefix(line, commandID) && (strings.HasPrefix(lineWithoutID, "NO ") || strings.HasPrefix(lineWithoutID, "BAD ")) {
debug.printErr(line)
err := errors.New(strings.Trim(lineWithoutID, "\r\n"))
ir.err = errors.Wrap(err, "IMAP error")
return
} else if command != "" && len(line) == 0 {
err := errors.New("empty answer")
ir.err = errors.Wrap(err, "IMAP error")
debug.printErr(ir.err.Error() + "\n")
return
}
debug.printRes(line)
if strings.HasPrefix(line, "* ") { //nolint[gocritic]
if section != "" {
ir.sections = append(ir.sections, section)
}
section = line
} else if strings.HasPrefix(line, commandID) {
if section != "" {
ir.sections = append(ir.sections, section)
}
ir.result = line
break
} else {
section += line
}
}
debug.printTime(time.Since(tstart))
}
func (ir *IMAPResponse) wait() {
for {
if ir.done {
break
}
time.Sleep(50 * time.Millisecond)
}
}
func (ir *IMAPResponse) AssertOK() *IMAPResponse {
ir.wait()
a.NoError(ir.t, ir.err)
return ir
}
func (ir *IMAPResponse) AssertError(wantErrMsg string) *IMAPResponse {
ir.wait()
if ir.err == nil {
a.Fail(ir.t, "Expected error %s", wantErrMsg)
} else {
a.Regexp(ir.t, wantErrMsg, ir.err.Error(), "Expected error %s but got %s", wantErrMsg, ir.err)
}
return ir
}
func (ir *IMAPResponse) AssertSectionsCount(expectedCount int) *IMAPResponse {
ir.wait()
a.Equal(ir.t, expectedCount, len(ir.sections))
return ir
}
// AssertSectionsInOrder checks sections against regular expression in exact order.
// First regexp checks first section, second the second and so on. If there is
// more responses (sections) than expected regexps, that's OK.
func (ir *IMAPResponse) AssertSectionsInOrder(wantRegexps ...string) *IMAPResponse {
ir.wait()
if !a.True(ir.t,
len(ir.sections) >= len(wantRegexps),
"Wrong number of sections, want %v, got %v",
len(wantRegexps),
len(ir.sections),
) {
return ir
}
for idx, wantRegexp := range wantRegexps {
section := ir.sections[idx]
match, err := regexp.MatchString(wantRegexp, section)
if !a.NoError(ir.t, err) {
return ir
}
if !a.True(ir.t, match, "Section does not match given regex", section, wantRegexp) {
return ir
}
}
return ir
}
// AssertSections is similar to AssertSectionsInOrder but is not strict to the order.
// It means it just tries to find all "regexps" in the response.
func (ir *IMAPResponse) AssertSections(wantRegexps ...string) *IMAPResponse {
ir.wait()
for _, wantRegexp := range wantRegexps {
a.NoError(ir.t, ir.hasSectionRegexp(wantRegexp), "regexp %v not found", wantRegexp)
}
return ir
}
// WaitForSections is the same as AssertSections but waits for `timeout` before giving up.
func (ir *IMAPResponse) WaitForSections(timeout time.Duration, wantRegexps ...string) {
a.Eventually(ir.t, func() bool {
return ir.HasSections(wantRegexps...)
}, timeout, 50*time.Millisecond, "Wanted sections: %v\nSections: %v", wantRegexps, &ir.sections)
}
// WaitForNotSections is the opposite of WaitForSection: waits to not have the response.
func (ir *IMAPResponse) WaitForNotSections(timeout time.Duration, unwantedRegexps ...string) *IMAPResponse {
time.Sleep(timeout)
match := ir.HasSections(unwantedRegexps...)
a.False(ir.t, match, "Unwanted sections: %v\nSections: %v", unwantedRegexps, &ir.sections)
return ir
}
// HasSections is the same as AssertSections but only returns bool (do not uses testingT).
func (ir *IMAPResponse) HasSections(wantRegexps ...string) bool {
for _, wantRegexp := range wantRegexps {
if err := ir.hasSectionRegexp(wantRegexp); err != nil {
return false
}
}
return true
}
func (ir *IMAPResponse) hasSectionRegexp(wantRegexp string) error {
for _, section := range ir.sections {
match, err := regexp.MatchString(wantRegexp, section)
if err != nil {
return err
}
if match {
return nil
}
}
return errors.New("Section matching given regex not found")
}

182
test/mocks/smtp.go Normal file
View File

@ -0,0 +1,182 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package mocks
import (
"bufio"
"bytes"
"fmt"
"io"
"net"
"net/mail"
"os"
"strings"
"sync"
"time"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type SMTPClient struct {
lock *sync.Mutex
debug *debug
t TestingT
conn net.Conn
response *bufio.Reader
address string
}
func NewSMTPClient(t TestingT, tag, smtpAddr string) *SMTPClient {
conn, err := net.Dial("tcp", smtpAddr)
require.NoError(t, err)
response := bufio.NewReader(conn)
// Read first response to opening connection.
_, err = response.ReadString('\n')
assert.NoError(t, err)
return &SMTPClient{
lock: &sync.Mutex{},
debug: newDebug(tag),
t: t,
conn: conn,
response: response,
}
}
func (c *SMTPClient) Close() {
c.lock.Lock()
defer c.lock.Unlock()
_ = c.conn.Close()
}
func (c *SMTPClient) SendCommands(commands ...string) *SMTPResponse {
c.lock.Lock()
defer c.lock.Unlock()
smtpResponse := &SMTPResponse{t: c.t}
for _, command := range commands {
tstart := time.Now()
c.debug.printReq(command)
fmt.Fprintf(c.conn, "%s\r\n", command)
message, err := c.response.ReadString('\n')
if err != nil {
smtpResponse.err = fmt.Errorf("read response failed: %v", err)
return smtpResponse
}
// Message contains code and message. Codes 4xx and 5xx are bad ones, except "500 Speak up".
if strings.HasPrefix(message, "4") || strings.HasPrefix(message, "5") {
c.debug.printErr(message)
err := errors.New(strings.Trim(message, "\r\n"))
smtpResponse.err = errors.Wrap(err, "SMTP error")
return smtpResponse
} else if command != "" && len(message) == 0 {
err := errors.New("empty answer")
smtpResponse.err = errors.Wrap(err, "SMTP error")
return smtpResponse
}
c.debug.printRes(message)
smtpResponse.result = message
c.debug.printTime(time.Since(tstart))
}
return smtpResponse
}
// Auth
func (c *SMTPClient) Login(account, password string) *SMTPResponse {
c.address = account
return c.SendCommands(
"HELO ATEIST.TEST",
"AUTH LOGIN",
base64(account),
base64(password),
)
}
func (c *SMTPClient) Logout() *SMTPResponse {
return c.SendCommands("QUIT")
}
// Sending
func (c *SMTPClient) EML(fileName, bcc string) *SMTPResponse {
f, err := os.Open(fileName) //nolint[gosec]
if err != nil {
panic(fmt.Errorf("smtp eml open: %s", err))
}
defer f.Close() //nolint[errcheck]
return c.SendMail(f, bcc)
}
func (c *SMTPClient) SendMail(r io.Reader, bcc string) *SMTPResponse {
var message, from string
var tos []string
if bcc != "" {
tos = append(tos, bcc)
}
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := string(bytes.Trim(scanner.Bytes(), "\r\n")) // Make sure no line ending is there.
message += line + "\r\n"
from = c.address
if from == "" && strings.HasPrefix(line, "From: ") {
if addr, err := mail.ParseAddress(line[6:]); err == nil {
from = addr.Address
}
}
if strings.HasPrefix(line, "To: ") || strings.HasPrefix(line, "CC: ") {
if addrs, err := mail.ParseAddressList(line[4:]); err == nil {
for _, addr := range addrs {
tos = append(tos, addr.Address)
}
}
}
}
if err := scanner.Err(); err != nil {
panic(fmt.Errorf("smtp eml scan: %s", err))
}
if from == "" {
panic(fmt.Errorf("smtp eml no from"))
}
if len(tos) == 0 {
panic(fmt.Errorf("smtp eml no to"))
}
commands := []string{
fmt.Sprintf("MAIL FROM:<%s>", from),
}
for _, to := range tos {
commands = append(commands, fmt.Sprintf("RCPT TO:<%s>", to))
}
commands = append(commands, "DATA", message+"\r\n.") // Message ending.
return c.SendCommands(commands...)
}

View File

@ -0,0 +1,42 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package mocks
import (
a "github.com/stretchr/testify/assert"
)
type SMTPResponse struct {
t TestingT
err error
result string
}
func (sr *SMTPResponse) AssertOK() *SMTPResponse {
a.NoError(sr.t, sr.err)
return sr
}
func (sr *SMTPResponse) AssertError(wantErrMsg string) *SMTPResponse {
if sr.err == nil {
a.Fail(sr.t, "Expected error %s", wantErrMsg)
} else {
a.Regexp(sr.t, wantErrMsg, sr.err.Error(), "Expected error %s but got %s", wantErrMsg, sr.err)
}
return sr
}

23
test/mocks/testingt.go Normal file
View File

@ -0,0 +1,23 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package mocks
type TestingT interface {
Errorf(string, ...interface{})
FailNow()
}

26
test/mocks/utils.go Normal file
View File

@ -0,0 +1,26 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package mocks
import (
b64 "encoding/base64"
)
func base64(data string) (encoded string) {
return b64.StdEncoding.EncodeToString([]byte(data))
}