GODT-1650: Send extras

This commit is contained in:
James Houlahan
2022-10-02 13:28:41 +02:00
parent 2cb739027b
commit ba9368426c
28 changed files with 1248 additions and 236 deletions

View File

@ -1,6 +1,8 @@
package tests
import (
"net/mail"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon/rfc822"
"gitlab.protontech.ch/go/liteapi"
@ -13,16 +15,16 @@ type API interface {
GetHostURL() string
AddCallWatcher(func(server.Call), ...string)
AddUser(username, password, address string) (string, string, error)
AddAddress(userID, address, password string) (string, error)
CreateUser(username, password, address string) (string, string, error)
CreateAddress(userID, address, password string) (string, error)
RemoveAddress(userID, addrID string) error
RevokeUser(userID string) error
GetLabels(userID string) ([]liteapi.Label, error)
AddLabel(userID, name string, labelType liteapi.LabelType) (string, error)
CreateLabel(userID, name string, labelType liteapi.LabelType) (string, error)
GetMessages(userID string) ([]liteapi.Message, error)
AddMessage(userID, addrID string, labelIDs []string, sender, recipient, subject, body string, mimeType rfc822.MIMEType, read, starred bool) (string, error)
CreateMessage(userID, addrID string, labelIDs []string, subject string, sender *mail.Address, toList, ccList, bccList []*mail.Address, decBody string, mimeType rfc822.MIMEType, read, starred bool) (string, error)
Close()
}

View File

@ -74,7 +74,8 @@ func TestFeatures(testingT *testing.T) {
ctx.Step(`^the internet is turned off$`, s.internetIsTurnedOff)
ctx.Step(`^the internet is turned on$`, s.internetIsTurnedOn)
ctx.Step(`^the user agent is "([^"]*)"$`, s.theUserAgentIs)
ctx.Step(`^the value of the "([^"]*)" header in the request to "([^"]*)" is "([^"]*)"$`, s.theValueOfTheHeaderInTheRequestToIs)
ctx.Step(`^the header in the "([^"]*)" request to "([^"]*)" has "([^"]*)" set to "([^"]*)"$`, s.theHeaderInTheRequestToHasSetTo)
ctx.Step(`^the body in the "([^"]*)" request to "([^"]*)" is:$`, s.theBodyInTheRequestToIs)
// ==== SETUP ====
ctx.Step(`^there exists an account with username "([^"]*)" and password "([^"]*)"$`, s.thereExistsAnAccountWithUsernameAndPassword)
@ -167,8 +168,9 @@ func TestFeatures(testingT *testing.T) {
ctx.Step(`^SMTP client "([^"]*)" cannot authenticate with incorrect password$`, s.smtpClientCannotAuthenticateWithIncorrectPassword)
ctx.Step(`^SMTP client "([^"]*)" sends MAIL FROM "([^"]*)"$`, s.smtpClientSendsMailFrom)
ctx.Step(`^SMTP client "([^"]*)" sends RCPT TO "([^"]*)"$`, s.smtpClientSendsRcptTo)
ctx.Step(`^SMTP client "([^"]*)" sends DATA "([^"]*)"$`, s.smtpClientSendsData)
ctx.Step(`^SMTP client "([^"]*)" sends DATA:$`, s.smtpClientSendsData)
ctx.Step(`^SMTP client "([^"]*)" sends RSET$`, s.smtpClientSendsReset)
ctx.Step(`^SMTP client "([^"]*)" sends the following message from "([^"]*)" to "([^"]*)":$`, s.smtpClientSendsTheFollowingMessageFromTo)
},
Options: &godog.Options{
Format: "pretty",

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/smtp"
"regexp"
"testing"
"github.com/Masterminds/semver/v3"
@ -172,20 +173,24 @@ func (t *testCtx) getMBoxID(userID string, name string) string {
return labels[idx].ID
}
func (t *testCtx) getLastCall(path string) (server.Call, error) {
calls := t.calls[len(t.calls)-2]
func (t *testCtx) getLastCall(method, path string) (server.Call, error) {
var allCalls []server.Call
if len(calls) == 0 {
for _, calls := range t.calls {
allCalls = append(allCalls, calls...)
}
if len(allCalls) == 0 {
return server.Call{}, fmt.Errorf("no calls made")
}
for _, call := range calls {
if call.URL.Path == path {
for idx := len(allCalls) - 1; idx >= 0; idx-- {
if call := allCalls[idx]; call.Method == method && regexp.MustCompile("^"+path+"$").MatchString(call.URL.Path) {
return call, nil
}
}
return calls[len(calls)-1], nil
return server.Call{}, fmt.Errorf("no call with method %q and path %q was made", method, path)
}
func (t *testCtx) pushError(err error) {

81
tests/diff.go Normal file
View File

@ -0,0 +1,81 @@
package tests
import (
"reflect"
"github.com/bradenaw/juniper/xslices"
)
func IsSub(outer, inner any) bool {
if outer == nil && inner != nil {
return IsSub(reflect.Zero(reflect.TypeOf(inner)).Interface(), inner)
}
if outer != nil && inner == nil {
return IsSub(reflect.Zero(reflect.TypeOf(outer)).Interface(), outer)
}
switch inner := inner.(type) {
case map[string]any:
outer, ok := outer.(map[string]any)
if !ok {
return false
}
return isSubMap(outer, inner)
case []any:
outer, ok := outer.([]any)
if !ok {
return false
}
if len(inner) != len(outer) {
return false
}
return isSubSlice(outer, inner)
default:
if reflect.TypeOf(outer) != reflect.TypeOf(inner) {
return false
}
if reflect.DeepEqual(outer, inner) {
return true
}
return reflect.DeepEqual(reflect.Zero(reflect.TypeOf(inner)).Interface(), inner)
}
}
func isSubMap(outer, inner map[string]any) bool {
for k, v := range inner {
w, ok := outer[k]
if !ok {
for _, w := range outer {
if IsSub(w, inner) {
return true
}
}
}
if !IsSub(w, v) {
return false
}
}
return true
}
func isSubSlice(outer, inner []any) bool {
for _, v := range inner {
if xslices.IndexFunc(outer, func(outer any) bool {
return IsSub(outer, v)
}) < 0 {
return false
}
}
return true
}

125
tests/diff_test.go Normal file
View File

@ -0,0 +1,125 @@
package tests
import (
"fmt"
"testing"
"github.com/goccy/go-json"
)
func Test_IsSub(t *testing.T) {
tests := []struct {
outer string
inner string
want bool
}{
{
outer: `{}`,
inner: `{}`,
want: true,
},
{
outer: `{"a": 1}`,
inner: `{"a": 1}`,
want: true,
},
{
outer: `{"a": 1, "b": 2}`,
inner: `{"a": 1}`,
want: true,
},
{
outer: `{"a": 1, "b": 2}`,
inner: `{"a": 1, "c": 3}`,
want: false,
},
{
outer: `{"a": 1, "b": {"c": 2}}`,
inner: `{"c": 2}`,
want: true,
},
{
outer: `{"a": 1, "b": {"c": 2, "d": 3}}`,
inner: `{"c": 2}`,
want: true,
},
{
outer: `{"a": 1, "b": {"c": 2, "d": 3}}`,
inner: `{"c": 2, "d": 3}`,
want: true,
},
{
outer: `{"a": 1, "b": {"c": 2, "d": 3}}`,
inner: `{"c": 2, "e": 3}`,
want: false,
},
{
outer: `{"a": 1, "b": {"c": 2, "d": "ignore"}}`,
inner: `{"a": 1, "b": {"c": 2, "d": ""}}`,
want: true,
},
{
outer: `{"a": 1, "b": {"c": 2, "d": null}}`,
inner: `{"a": 1, "b": {"c": 2, "d": null}}`,
want: true,
},
{
outer: `{"a": 1, "b": {"c": 2, "d": ["1"]}}`,
inner: `{"a": 1, "b": {"c": 2, "d": []}}`,
want: false,
},
{
outer: `{"a": 1, "b": {"c": 2, "d": []}}`,
inner: `{"a": 1, "b": {"c": 2, "d": null}}`,
want: true,
},
{
outer: `{"a": []}`,
inner: `{"a": []}`,
want: true,
},
{
outer: `{"a": [1, 2]}`,
inner: `{"a": [1, 2]}`,
want: true,
},
{
outer: `{"a": [1, 3]}`,
inner: `{"a": [1, 2]}`,
want: false,
},
{
outer: `{"a": [1, 2, 3]}`,
inner: `{"a": [1, 2]}`,
want: false,
},
{
outer: `{"a": null}`,
inner: `{"a": []}`,
want: true,
},
{
outer: `{"a": []}`,
inner: `{"a": null}`,
want: true,
},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("%v vs %v", tt.inner, tt.outer), func(t *testing.T) {
var outerMap, innerMap map[string]any
if err := json.Unmarshal([]byte(tt.outer), &outerMap); err != nil {
t.Fatal(err)
}
if err := json.Unmarshal([]byte(tt.inner), &innerMap); err != nil {
t.Fatal(err)
}
if got := IsSub(outerMap, innerMap); got != tt.want {
t.Errorf("isSub() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -1,8 +1,11 @@
package tests
import (
"encoding/json"
"fmt"
"strings"
"github.com/cucumber/godog"
)
func (s *scenario) itSucceeds() error {
@ -52,15 +55,38 @@ func (s *scenario) theUserAgentIs(userAgent string) error {
return nil
}
func (s *scenario) theValueOfTheHeaderInTheRequestToIs(key, path, value string) error {
call, err := s.t.getLastCall(path)
func (s *scenario) theHeaderInTheRequestToHasSetTo(method, path, key, value string) error {
call, err := s.t.getLastCall(method, path)
if err != nil {
return err
}
if haveKey := call.Request.Header.Get(key); haveKey != value {
if haveKey := call.Header.Get(key); haveKey != value {
return fmt.Errorf("have header %q, want %q", haveKey, value)
}
return nil
}
func (s *scenario) theBodyInTheRequestToIs(method, path string, value *godog.DocString) error {
call, err := s.t.getLastCall(method, path)
if err != nil {
return err
}
var body, want map[string]any
if err := json.Unmarshal(call.Body, &body); err != nil {
return err
}
if err := json.Unmarshal([]byte(value.Content), &want); err != nil {
return err
}
if !IsSub(body, want) {
return fmt.Errorf("have body %v, want %v", body, want)
}
return nil
}

View File

@ -17,4 +17,4 @@ Feature: The IMAP ID is propagated to bridge
When user "user@pm.me" connects IMAP client "1"
And IMAP client "1" announces its ID with name "name" and version "version"
When the user reports a bug
Then the value of the "User-Agent" header in the request to "/core/v4/reports/bug" is "name/version ([GOOS])"
Then the header in the "POST" request to "/core/v4/reports/bug" has "User-Agent" set to "name/version ([GOOS])"

View File

@ -6,7 +6,10 @@ Feature: SMTP initiation
When user "user@pm.me" connects and authenticates SMTP client "1"
Scenario: Send without first announcing FROM and TO
When SMTP client "1" sends DATA "Subject: test"
When SMTP client "1" sends DATA:
"""
Subject: test
"""
Then it fails with error "Missing RCPT TO command"
Scenario: Reset is the same as without FROM and TO
@ -16,7 +19,10 @@ Feature: SMTP initiation
Then it succeeds
When SMTP client "1" sends RSET
Then it succeeds
When SMTP client "1" sends DATA "Subject: test"
When SMTP client "1" sends DATA:
"""
Subject: test
"""
Then it fails with error "Missing RCPT TO command"
Scenario: Send without FROM
@ -26,7 +32,10 @@ Feature: SMTP initiation
Scenario: Send without TO
When SMTP client "1" sends MAIL FROM "<user@pm.me>"
Then it succeeds
When SMTP client "1" sends DATA "Subject: test"
When SMTP client "1" sends DATA:
"""
Subject: test
"""
Then it fails with error "Missing RCPT TO command"
Scenario: Send with empty FROM

View File

@ -0,0 +1,332 @@
Feature: SMTP sending of plain messages
Background:
Given there exists an account with username "user@pm.me" and password "password"
And there exists an account with username "bridgetest@protonmail.com" and password "password"
And there exists an account with username "bridgetest2@protonmail.com" and password "password"
And bridge starts
And the user logs in with username "user@pm.me" and password "password"
And user "user@pm.me" connects and authenticates SMTP client "1"
Scenario: Only from and to headers to internal account
When SMTP client "1" sends the following message from "user@pm.me" to "bridgetest@protonmail.com":
"""
From: Bridge Test <user@pm.me>
To: Internal Bridge <bridgetest@protonmail.com>
hello
"""
Then it succeeds
When user "user@pm.me" connects and authenticates IMAP client "1"
Then IMAP client "1" eventually sees the following messages in "Sent":
| sender | recipient | subject | unread |
| user@pm.me | bridgetest@protonmail.com | | false |
And the body in the "POST" request to "/mail/v4/messages" is:
"""
{
"Message": {
"Subject": "",
"Sender": {
"Name": "Bridge Test"
},
"ToList": [
{
"Address": "bridgetest@protonmail.com",
"Name": "Internal Bridge"
}
],
"CCList": [],
"BCCList": [],
"MIMEType": "text/plain"
}
}
"""
Scenario: Only from and to headers to external account
When SMTP client "1" sends the following message from "user@pm.me" to "pm.bridge.qa@gmail.com":
"""
From: Bridge Test <user@pm.me>
To: External Bridge <pm.bridge.qa@gmail.com>
hello
"""
Then it succeeds
When user "user@pm.me" connects and authenticates IMAP client "1"
Then IMAP client "1" eventually sees the following messages in "Sent":
| sender | recipient | subject | unread |
| user@pm.me | pm.bridge.qa@gmail.com | | false |
And the body in the "POST" request to "/mail/v4/messages" is:
"""
{
"Message": {
"Subject": "",
"Sender": {
"Name": "Bridge Test"
},
"ToList": [
{
"Address": "pm.bridge.qa@gmail.com",
"Name": "External Bridge"
}
],
"CCList": [],
"BCCList": [],
"MIMEType": "text/plain"
}
}
"""
Scenario: Basic message to internal account
When SMTP client "1" sends the following message from "user@pm.me" to "bridgetest@protonmail.com":
"""
From: Bridge Test <user@pm.me>
To: Internal Bridge <bridgetest@protonmail.com>
Subject: Plain text internal
Content-Disposition: inline
Content-Type: text/plain; charset=utf-8
This is body of mail 👋
"""
Then it succeeds
When user "user@pm.me" connects and authenticates IMAP client "1"
Then IMAP client "1" eventually sees the following messages in "Sent":
| sender | recipient | subject | unread |
| user@pm.me | bridgetest@protonmail.com | Plain text internal | false |
And the body in the "POST" request to "/mail/v4/messages" is:
"""
{
"Message": {
"Subject": "Plain text internal",
"Sender": {
"Name": "Bridge Test"
},
"ToList": [
{
"Address": "bridgetest@protonmail.com",
"Name": "Internal Bridge"
}
],
"CCList": [],
"BCCList": [],
"MIMEType": "text/plain"
}
}
"""
Scenario: Basic message to external account
When SMTP client "1" sends the following message from "user@pm.me" to "pm.bridge.qa@gmail.com":
"""
From: Bridge Test <user@pm.me>
To: External Bridge <pm.bridge.qa@gmail.com>
Subject: Plain text external
Content-Disposition: inline
Content-Type: text/plain; charset=utf-8
This is body of mail 👋
"""
Then it succeeds
When user "user@pm.me" connects and authenticates IMAP client "1"
Then IMAP client "1" eventually sees the following messages in "Sent":
| sender | recipient | subject | unread |
| user@pm.me | pm.bridge.qa@gmail.com | Plain text external | false |
And the body in the "POST" request to "/mail/v4/messages" is:
"""
{
"Message": {
"Subject": "Plain text external",
"Sender": {
"Name": "Bridge Test"
},
"ToList": [
{
"Address": "pm.bridge.qa@gmail.com",
"Name": "External Bridge"
}
],
"CCList": [],
"BCCList": [],
"MIMEType": "text/plain"
}
}
"""
Scenario: Message without charset is utf8
When SMTP client "1" sends the following message from "user@pm.me" to "pm.bridge.qa@gmail.com":
"""
From: Bridge Test <user@pm.me>
To: External Bridge <pm.bridge.qa@gmail.com>
Subject: Plain text no charset external
Content-Disposition: inline
Content-Type: text/plain;
This is body of mail without charset. Please assume utf8
"""
Then it succeeds
When user "user@pm.me" connects and authenticates IMAP client "1"
Then IMAP client "1" eventually sees the following messages in "Sent":
| sender | recipient | subject | unread |
| user@pm.me | pm.bridge.qa@gmail.com | Plain text no charset external | false |
And the body in the "POST" request to "/mail/v4/messages" is:
"""
{
"Message": {
"Subject": "Plain text no charset external",
"Sender": {
"Name": "Bridge Test"
},
"ToList": [
{
"Address": "pm.bridge.qa@gmail.com",
"Name": "External Bridge"
}
],
"CCList": [],
"BCCList": [],
"MIMEType": "text/plain"
}
}
"""
Scenario: Message without charset is base64-encoded latin1
When SMTP client "1" sends the following message from "user@pm.me" to "pm.bridge.qa@gmail.com":
"""
From: Bridge Test <user@pm.me>
To: External Bridge <pm.bridge.qa@gmail.com>
Subject: Plain text no charset external
Content-Disposition: inline
Content-Type: text/plain;
Content-Transfer-Encoding: base64
dGhpcyBpcyBpbiBsYXRpbjEgYW5kIHRoZXJlIGFyZSBsb3RzIG9mIGVzIHdpdGggYWNjZW50czog
6enp6enp6enp6enp6enp
"""
Then it succeeds
When user "user@pm.me" connects and authenticates IMAP client "1"
Then IMAP client "1" eventually sees the following messages in "Sent":
| sender | recipient | subject | unread |
| user@pm.me | pm.bridge.qa@gmail.com | Plain text no charset external | false |
And the body in the "POST" request to "/mail/v4/messages" is:
"""
{
"Message": {
"Subject": "Plain text no charset external",
"Sender": {
"Name": "Bridge Test"
},
"ToList": [
{
"Address": "pm.bridge.qa@gmail.com",
"Name": "External Bridge"
}
],
"CCList": [],
"BCCList": [],
"MIMEType": "text/plain"
}
}
"""
Scenario: Message without charset and content is detected as HTML
When SMTP client "1" sends the following message from "user@pm.me" to "pm.bridge.qa@gmail.com":
"""
From: Bridge Test <user@pm.me>
To: External Bridge <pm.bridge.qa@gmail.com>
Subject: Plain, no charset, no content, external
Content-Disposition: inline
Content-Type: text/plain;
"""
Then it succeeds
When user "user@pm.me" connects and authenticates IMAP client "1"
Then IMAP client "1" eventually sees the following messages in "Sent":
| sender | recipient | subject | unread |
| user@pm.me | pm.bridge.qa@gmail.com | Plain, no charset, no content, external | false |
And the body in the "POST" request to "/mail/v4/messages" is:
"""
{
"Message": {
"Subject": "Plain, no charset, no content, external",
"Sender": {
"Name": "Bridge Test"
},
"ToList": [
{
"Address": "pm.bridge.qa@gmail.com",
"Name": "External Bridge"
}
],
"CCList": [],
"BCCList": [],
"MIMEType": "text/plain"
}
}
"""
Scenario: RCPT does not contain all CC
When SMTP client "1" sends MAIL FROM "<user@pm.me>"
And SMTP client "1" sends RCPT TO "<bridgetest@protonmail.com>"
And SMTP client "1" sends DATA:
"""
From: Bridge Test <user@pm.me>
To: Internal Bridge <bridgetest@protonmail.com>
CC: Internal Bridge 2 <bridgetest2@protonmail.com>
Content-Type: text/plain
Subject: RCPT-CC test
This is CC missing in RCPT test. Have a nice day!
.
"""
Then it succeeds
When user "user@pm.me" connects and authenticates IMAP client "1"
Then IMAP client "1" eventually sees the following messages in "Sent":
| sender | recipient | cc | subject | unread |
| user@pm.me | bridgetest@protonmail.com | bridgetest2@protonmail.com | RCPT-CC test | false |
And the body in the "POST" request to "/mail/v4/messages" is:
"""
{
"Message": {
"Subject": "RCPT-CC test",
"Sender": {
"Name": "Bridge Test"
},
"ToList": [
{
"Address": "bridgetest@protonmail.com",
"Name": "Internal Bridge"
}
],
"CCList": [
{
"Address": "bridgetest2@protonmail.com",
"Name": "Internal Bridge 2"
}
],
"BCCList": []
}
}
"""
And the body in the "POST" request to "/mail/v4/messages/.*" is:
"""
{
"Packages":[
{
"Addresses":{
"bridgetest@protonmail.com":{
"Type":1
},
"bridgetest2@protonmail.com":{
"Type":1
}
},
"Type":1,
"MIMEType":"text/plain"
}
]
}
"""

View File

@ -280,12 +280,28 @@ func (s *scenario) imapClientSeesTheFollowingMessagesInMailbox(clientID, mailbox
}
haveMessages := xslices.Map(fetch, func(msg *imap.Message) Message {
return Message{
Sender: msg.Envelope.Sender[0].Address(),
Recipient: msg.Envelope.To[0].Address(),
Subject: msg.Envelope.Subject,
Unread: slices.Contains(msg.Flags, imap.SeenFlag),
message := Message{
Subject: msg.Envelope.Subject,
Unread: slices.Contains(msg.Flags, imap.SeenFlag),
}
if len(msg.Envelope.From) > 0 {
message.From = msg.Envelope.From[0].Address()
}
if len(msg.Envelope.To) > 0 {
message.To = msg.Envelope.To[0].Address()
}
if len(msg.Envelope.Cc) > 0 {
message.CC = msg.Envelope.Cc[0].Address()
}
if len(msg.Envelope.Bcc) > 0 {
message.BCC = msg.Envelope.Bcc[0].Address()
}
return message
})
return matchMessages(haveMessages, table)

View File

@ -5,6 +5,7 @@ import (
"net/smtp"
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
"github.com/cucumber/godog"
)
func (s *scenario) userConnectsSMTPClient(username, clientID string) error {
@ -87,13 +88,13 @@ func (s *scenario) smtpClientSendsRcptTo(clientID, to string) error {
return nil
}
func (s *scenario) smtpClientSendsData(clientID, data string) error {
func (s *scenario) smtpClientSendsData(clientID string, data *godog.DocString) error {
_, client := s.t.getSMTPClient(clientID)
rc, err := client.Data()
if err != nil {
s.t.pushError(err)
} else if _, err := rc.Write([]byte(data)); err != nil {
} else if _, err := rc.Write([]byte(data.Content)); err != nil {
s.t.pushError(err)
} else if err := rc.Close(); err != nil {
s.t.pushError(err)
@ -109,3 +110,30 @@ func (s *scenario) smtpClientSendsReset(clientID string) error {
return nil
}
func (s *scenario) smtpClientSendsTheFollowingMessageFromTo(clientID, from, to string, message *godog.DocString) error {
_, client := s.t.getSMTPClient(clientID)
s.t.pushError(func() error {
if err := client.Mail(from); err != nil {
return err
}
if err := client.Rcpt(to); err != nil {
return err
}
wc, err := client.Data()
if err != nil {
return err
}
if _, err := wc.Write([]byte(message.Content)); err != nil {
return err
}
return wc.Close()
}())
return nil
}

View File

@ -2,6 +2,7 @@ package tests
import (
"fmt"
"reflect"
"strconv"
"time"
@ -13,10 +14,24 @@ import (
)
type Message struct {
Sender string
Recipient string
Subject string
Unread bool
Subject string `bdd:"subject"`
From string `bdd:"sender"`
To string `bdd:"recipient"`
CC string `bdd:"cc"`
BCC string `bdd:"bcc"`
Unread bool `bdd:"unread"`
}
func newMessageFromRow(header, row *messages.PickleTableRow) Message {
var msg Message
if err := unmarshalRow(header, row, &msg); err != nil {
panic(err)
}
return msg
}
func matchMessages(have []Message, want *godog.Table) error {
@ -28,20 +43,27 @@ func matchMessages(have []Message, want *godog.Table) error {
}
func parseMessages(table *godog.Table) []Message {
header := table.Rows[0]
return xslices.Map(table.Rows[1:], func(row *messages.PickleTableRow) Message {
return Message{
Sender: row.Cells[0].Value,
Recipient: row.Cells[1].Value,
Subject: row.Cells[2].Value,
Unread: mustParseBool(row.Cells[3].Value),
}
return newMessageFromRow(header, row)
})
}
type Mailbox struct {
Name string
Total int
Unread int
Name string `bdd:"name"`
Total int `bdd:"total"`
Unread int `bdd:"unread"`
}
func newMailboxFromRow(header, row *messages.PickleTableRow) Mailbox {
var mbox Mailbox
if err := unmarshalRow(header, row, &mbox); err != nil {
panic(err)
}
return mbox
}
func matchMailboxes(have []Mailbox, want *godog.Table) error {
@ -53,33 +75,13 @@ func matchMailboxes(have []Mailbox, want *godog.Table) error {
}
func parseMailboxes(table *godog.Table) []Mailbox {
mustParseInt := func(s string) int {
i, err := strconv.Atoi(s)
if err != nil {
panic(err)
}
return i
}
header := table.Rows[0]
return xslices.Map(table.Rows[1:], func(row *messages.PickleTableRow) Mailbox {
return Mailbox{
Name: row.Cells[0].Value,
Total: mustParseInt(row.Cells[1].Value),
Unread: mustParseInt(row.Cells[2].Value),
}
return newMailboxFromRow(header, row)
})
}
func mustParseBool(s string) bool {
v, err := strconv.ParseBool(s)
if err != nil {
panic(err)
}
return v
}
func eventually(condition func() error, waitFor, tick time.Duration) error {
ch := make(chan error, 1)
@ -108,3 +110,62 @@ func eventually(condition func() error, waitFor, tick time.Duration) error {
}
}
}
func getCellValue(header, row *messages.PickleTableRow, name string) (string, bool) {
for idx, cell := range header.Cells {
if cell.Value == name {
return row.Cells[idx].Value, true
}
}
return "", false
}
func unmarshalRow(header, row *messages.PickleTableRow, v any) error {
typ := reflect.TypeOf(v).Elem()
for idx := 0; idx < typ.NumField(); idx++ {
field := typ.Field(idx)
if tag, ok := field.Tag.Lookup("bdd"); ok {
cell, ok := getCellValue(header, row, tag)
if !ok {
continue
}
switch field.Type.Kind() {
case reflect.String:
reflect.ValueOf(v).Elem().Field(idx).SetString(cell)
case reflect.Int:
reflect.ValueOf(v).Elem().Field(idx).SetInt(int64(mustParseInt(cell)))
case reflect.Bool:
reflect.ValueOf(v).Elem().Field(idx).SetBool(mustParseBool(cell))
default:
return fmt.Errorf("unsupported type %q", field.Type.Kind())
}
}
}
return nil
}
func mustParseInt(s string) int {
i, err := strconv.Atoi(s)
if err != nil {
panic(err)
}
return i
}
func mustParseBool(s string) bool {
v, err := strconv.ParseBool(s)
if err != nil {
panic(err)
}
return v
}

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"net/mail"
"time"
"github.com/ProtonMail/gluon/rfc822"
@ -17,7 +18,7 @@ import (
func (s *scenario) thereExistsAnAccountWithUsernameAndPassword(username, password string) error {
// Create the user.
userID, addrID, err := s.t.api.AddUser(username, password, username)
userID, addrID, err := s.t.api.CreateUser(username, password, username)
if err != nil {
return err
}
@ -37,7 +38,7 @@ func (s *scenario) thereExistsAnAccountWithUsernameAndPassword(username, passwor
func (s *scenario) theAccountHasAdditionalAddress(username, address string) error {
userID := s.t.getUserID(username)
addrID, err := s.t.api.AddAddress(userID, address, s.t.getUserPass(userID))
addrID, err := s.t.api.CreateAddress(userID, address, s.t.getUserPass(userID))
if err != nil {
return err
}
@ -62,7 +63,7 @@ func (s *scenario) theAccountNoLongerHasAdditionalAddress(username, address stri
func (s *scenario) theAccountHasCustomFolders(username string, count int) error {
for idx := 0; idx < count; idx++ {
if _, err := s.t.api.AddLabel(s.t.getUserID(username), uuid.NewString(), liteapi.LabelTypeFolder); err != nil {
if _, err := s.t.api.CreateLabel(s.t.getUserID(username), uuid.NewString(), liteapi.LabelTypeFolder); err != nil {
return err
}
}
@ -72,7 +73,7 @@ func (s *scenario) theAccountHasCustomFolders(username string, count int) error
func (s *scenario) theAccountHasCustomLabels(username string, count int) error {
for idx := 0; idx < count; idx++ {
if _, err := s.t.api.AddLabel(s.t.getUserID(username), uuid.NewString(), liteapi.LabelTypeLabel); err != nil {
if _, err := s.t.api.CreateLabel(s.t.getUserID(username), uuid.NewString(), liteapi.LabelTypeLabel); err != nil {
return err
}
}
@ -103,7 +104,7 @@ func (s *scenario) theAccountHasTheFollowingCustomMailboxes(username string, tab
})
for _, wantMailbox := range wantMailboxes {
if _, err := s.t.api.AddLabel(s.t.getUserID(username), wantMailbox.name, wantMailbox.typ); err != nil {
if _, err := s.t.api.CreateLabel(s.t.getUserID(username), wantMailbox.name, wantMailbox.typ); err != nil {
return err
}
}
@ -117,13 +118,15 @@ func (s *scenario) theAddressOfAccountHasTheFollowingMessagesInMailbox(address,
mboxID := s.t.getMBoxID(userID, mailbox)
for _, wantMessage := range parseMessages(table) {
if _, err := s.t.api.AddMessage(
if _, err := s.t.api.CreateMessage(
userID,
addrID,
[]string{mboxID},
wantMessage.Sender,
wantMessage.Recipient,
wantMessage.Subject,
&mail.Address{Address: wantMessage.From},
[]*mail.Address{{Address: wantMessage.To}},
[]*mail.Address{},
[]*mail.Address{},
"some body goes here",
rfc822.TextPlain,
wantMessage.Unread,
@ -142,16 +145,18 @@ func (s *scenario) theAddressOfAccountHasMessagesInMailbox(address, username str
mboxID := s.t.getMBoxID(userID, mailbox)
for idx := 0; idx < count; idx++ {
if _, err := s.t.api.AddMessage(
if _, err := s.t.api.CreateMessage(
userID,
addrID,
[]string{mboxID},
fmt.Sprintf("sender%v@pm.me", idx),
fmt.Sprintf("recipient%v@pm.me", idx),
fmt.Sprintf("subject %v", idx),
fmt.Sprintf("body %v", idx),
fmt.Sprintf("subject %d", idx),
&mail.Address{Address: fmt.Sprintf("sender %d", idx)},
[]*mail.Address{{Address: fmt.Sprintf("recipient %d", idx)}},
[]*mail.Address{},
[]*mail.Address{},
fmt.Sprintf("body %d", idx),
rfc822.TextPlain,
false,
idx%2 == 0,
false,
); err != nil {
return err