mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-11 13:16:53 +00:00
We build too many walls and not enough bridges
This commit is contained in:
41
test/Makefile
Normal file
41
test/Makefile
Normal file
@ -0,0 +1,41 @@
|
||||
.PHONY: check-has-go install-godog test test-live test-debug test-live-debug
|
||||
|
||||
export GO111MODULE=on
|
||||
export VERSION:=1.2.5-integrationtest
|
||||
export VERBOSITY?=fatal
|
||||
export TEST_DATA=testdata
|
||||
|
||||
check-has-go:
|
||||
@which go || (echo "Install Go-lang!" && exit 1)
|
||||
|
||||
install-godog: check-has-go
|
||||
go get github.com/cucumber/godog/cmd/godog@v0.8.1
|
||||
|
||||
test:
|
||||
which godog || $(MAKE) install-godog
|
||||
TEST_ENV=fake TEST_ACCOUNTS=accounts/fake.json godog --tags="~@ignore" $(FEATURES)
|
||||
|
||||
# Doesn't work in parallel!
|
||||
# Provide TEST_ACCOUNTS with your accounts.
|
||||
test-live:
|
||||
which godog || $(MAKE) install-godog
|
||||
TEST_ENV=live godog --tags="~@ignore && ~@ignore-live" $(FEATURES)
|
||||
|
||||
# Doesn't work in parallel!
|
||||
# Provide TEST_ACCOUNTS with your accounts.
|
||||
# We need to pass build tag which is not possible with godog command.
|
||||
# Tests against staging env are intended for debug purposes or checking new changes on API.
|
||||
test-stage:
|
||||
TEST_ENV=live go test -tags=$(TAGS) -- $(FEATURES)
|
||||
|
||||
test-debug:
|
||||
TEST_ENV=fake dlv test -- $(FEATURES)
|
||||
|
||||
test-live-debug:
|
||||
TEST_ENV=live dlv test -- $(FEATURES)
|
||||
|
||||
# -run flag is not working anyway, but lets keep it there to note we really do not want to run tests.
|
||||
# To properly benchmark sync/fetch, we need everything empty. For that is better to start everything
|
||||
# again and safest way is to run only one loop per run.
|
||||
bench:
|
||||
TEST_DATA=../testdata go test -run='^$$' -bench=. -benchtime=1x -timeout=60m ./benchmarks/...
|
||||
130
test/README.md
Normal file
130
test/README.md
Normal file
@ -0,0 +1,130 @@
|
||||
# Integration tests
|
||||
|
||||
This folder contains integration tests of the Bridge app.
|
||||
|
||||
## What and how we are testing
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
S[Server]
|
||||
C[Client]
|
||||
U[User]
|
||||
Creds[Credentials store]
|
||||
|
||||
subgraph "Bridge app"
|
||||
Core[Bridge core]
|
||||
Store
|
||||
Frontend["Qt / CLI"]
|
||||
IMAP
|
||||
SMTP
|
||||
API[PMAPI]
|
||||
|
||||
IMAP --> Core
|
||||
SMTP --> Core
|
||||
Frontend --> Core
|
||||
Store --> Core
|
||||
Core --> API
|
||||
end
|
||||
|
||||
C --> IMAP
|
||||
C --> SMTP
|
||||
U --> Frontend
|
||||
API --> S
|
||||
Core --> Creds
|
||||
```
|
||||
|
||||
We want to test Bridge app from outside as much as possible. So we mock server (API),
|
||||
credentials store and call commands to IMAP or SMTP the same way as client would do.
|
||||
|
||||
## Example test
|
||||
|
||||
BDD test in gherkin (cucumber) format (https://cucumber.io/docs/gherkin/reference/).
|
||||
|
||||
```
|
||||
Feature: IMAP update messages
|
||||
Background:
|
||||
Given there is connected user "user"
|
||||
And there are messages in mailbox "INBOX" for "user"
|
||||
| from | to | subject | body | read | starred |
|
||||
| john.doe@mail.com | user@pm.me | foo | hello | false | false |
|
||||
| jane.doe@mail.com | name@pm.me | bar | world | true | true |
|
||||
And there is IMAP client logged in as "user"
|
||||
And there is IMAP client selected in "INBOX"
|
||||
|
||||
Scenario: Mark message as read
|
||||
When IMAP client marks message "1" as read
|
||||
Then IMAP response is "OK"
|
||||
And message "1" in "INBOX" for "user" is marked as read
|
||||
And message "1" in "INBOX" for "user" is marked as unstarred
|
||||
```
|
||||
|
||||
Is translated into code with godog (https://github.com/cucumber/godog/).
|
||||
|
||||
```go
|
||||
// Registration
|
||||
func FeatureContext(s *godog.Suite) {
|
||||
s.Step(`^there is connected user "([^"]*)"$`, thereIsConnectedUser)
|
||||
}
|
||||
|
||||
// Godog step function
|
||||
func thereIsConnectedUser(username string) error {
|
||||
account := ctx.GetTestAccount(username)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
ctx.GetPMAPIController().AddUser(account.User, account.Addresses)
|
||||
return ctx.LoginUser(account.Username(), account.Password(), account.MailboxPassword())
|
||||
}
|
||||
```
|
||||
|
||||
## BDD
|
||||
|
||||
BDD has three parts:
|
||||
|
||||
* `Given` (setup),
|
||||
* `When` (action)
|
||||
* and `Then` (check).
|
||||
|
||||
Setup has to prepare context and always end without error. Action, on
|
||||
the other hand, needs to always end without error, but store it in
|
||||
the context. Check should analyze the status of the bridge, store or
|
||||
API and also check whether something failed before.
|
||||
|
||||
Therefore we cannot use a sentence such as `there is user` for both
|
||||
setup and check steps. We always begin setup steps with `there is/are`,
|
||||
while check steps are written in the form `something is/has feature`.
|
||||
Actions are written in the form `something does action`. By doing this
|
||||
we can always be sure what each steps does or should do.
|
||||
|
||||
In the code, we separate those parts in its own files to make sure
|
||||
it's clear how the function should be implemented.
|
||||
|
||||
## API faked by fakeapi or liveapi
|
||||
|
||||
We need to control what server returns. Instead of using raw JSONs,
|
||||
we fake the whole pmapi for local testing. Fake pmapi behaves as much
|
||||
as possible the same way as real server, but does not follow every
|
||||
single detail. Otherwise we would end up with writing complete server. :-)
|
||||
|
||||
For both -- fake local pmapi and real live server -- we use controller.
|
||||
Controller is available on test context and does setup like setting up
|
||||
internet connection, user settings, labels or messages.
|
||||
|
||||
Accounts for each environment are set up in `accounts` folder. Each
|
||||
test function should use `TestAccount` object obtained by test ID
|
||||
(such as `user` or `userMultipleAddress` for users, or `primary`
|
||||
or `secondary` for addresses) and use available functions to get real
|
||||
IDs (even if fake API uses the test IDs as real ones).
|
||||
|
||||
Testing against live is using real users and doesn't work in parallel.
|
||||
Only one job against live at a time can be running.
|
||||
|
||||
## External e-mail accounts
|
||||
|
||||
We have some external accounts which we are using for testing:
|
||||
|
||||
* pm.bridge.qa@gmail.com
|
||||
* bridge-qa@yandex.ru
|
||||
* bridgeqa@seznam.cz
|
||||
|
||||
For access, ask bridge team.
|
||||
192
test/accounts/account.go
Normal file
192
test/accounts/account.go
Normal file
@ -0,0 +1,192 @@
|
||||
// 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 accounts
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
|
||||
const (
|
||||
testUserKey = "user_key.json"
|
||||
testAddressKey = "address_key.json"
|
||||
testKeyPassphrase = "testpassphrase"
|
||||
)
|
||||
|
||||
type TestAccount struct {
|
||||
user *pmapi.User
|
||||
addressToBeUsed *pmapi.Address
|
||||
addressesByBDDAddressID map[string]*pmapi.Address
|
||||
password string
|
||||
mailboxPassword string
|
||||
twoFAEnabled bool
|
||||
}
|
||||
|
||||
func newTestAccount(
|
||||
user *pmapi.User,
|
||||
addressesByBDDAddressID map[string]*pmapi.Address,
|
||||
addressIDToBeUsed string,
|
||||
password,
|
||||
mailboxPassword string,
|
||||
twoFAEnabled bool,
|
||||
) *TestAccount {
|
||||
account := &TestAccount{
|
||||
user: user,
|
||||
addressesByBDDAddressID: addressesByBDDAddressID,
|
||||
password: password,
|
||||
mailboxPassword: mailboxPassword,
|
||||
twoFAEnabled: twoFAEnabled,
|
||||
}
|
||||
|
||||
if addressIDToBeUsed == "" {
|
||||
account.addressToBeUsed = account.Addresses().Main()
|
||||
} else {
|
||||
for addressID, address := range addressesByBDDAddressID {
|
||||
if addressID == addressIDToBeUsed {
|
||||
account.addressToBeUsed = address
|
||||
}
|
||||
}
|
||||
}
|
||||
if account.addressToBeUsed == nil {
|
||||
// Return nothing which will be interpreted as not implemented the same way the whole account.
|
||||
return nil
|
||||
}
|
||||
|
||||
account.initKeys()
|
||||
return account
|
||||
}
|
||||
|
||||
func (a *TestAccount) initKeys() {
|
||||
if a.user.Keys.Keys != nil {
|
||||
return
|
||||
}
|
||||
userKeys := loadPMKeys(readTestFile(testUserKey))
|
||||
_ = userKeys.KeyRing.Unlock([]byte(testKeyPassphrase))
|
||||
|
||||
addressKeys := loadPMKeys(readTestFile(testAddressKey))
|
||||
_ = addressKeys.KeyRing.Unlock([]byte(testKeyPassphrase))
|
||||
|
||||
a.user.Keys = *userKeys
|
||||
for _, addressEmail := range a.Addresses().ActiveEmails() {
|
||||
a.Addresses().ByEmail(addressEmail).Keys = *addressKeys
|
||||
}
|
||||
}
|
||||
|
||||
func readTestFile(fileName string) []byte {
|
||||
testDataFolder := os.Getenv("TEST_DATA")
|
||||
path := filepath.Join(testDataFolder, fileName)
|
||||
data, err := ioutil.ReadFile(path) //nolint[gosec]
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func loadPMKeys(jsonKeys []byte) (keys *pmapi.PMKeys) {
|
||||
_ = json.Unmarshal(jsonKeys, &keys)
|
||||
return
|
||||
}
|
||||
|
||||
func (a *TestAccount) User() *pmapi.User {
|
||||
return a.user
|
||||
}
|
||||
|
||||
func (a *TestAccount) UserID() string {
|
||||
return a.user.ID
|
||||
}
|
||||
|
||||
func (a *TestAccount) Username() string {
|
||||
return a.user.Name
|
||||
}
|
||||
|
||||
func (a *TestAccount) Addresses() *pmapi.AddressList {
|
||||
addressArray := []*pmapi.Address{}
|
||||
for _, address := range a.addressesByBDDAddressID {
|
||||
addressArray = append(addressArray, address)
|
||||
}
|
||||
// The order of addresses is important in PMAPI because the primary
|
||||
// address is always the first in array. We are using map to define
|
||||
// testing addresses which can cause random re-schuffle between tests
|
||||
sort.SliceStable(
|
||||
addressArray,
|
||||
func(i, j int) bool {
|
||||
return addressArray[i].Order < addressArray[j].Order
|
||||
},
|
||||
)
|
||||
addresses := pmapi.AddressList(addressArray)
|
||||
return &addresses
|
||||
}
|
||||
|
||||
func (a *TestAccount) Address() string {
|
||||
return a.addressToBeUsed.Email
|
||||
}
|
||||
|
||||
func (a *TestAccount) AddressID() string {
|
||||
return a.addressToBeUsed.ID
|
||||
}
|
||||
|
||||
// EnsureAddressID accepts address (simply the address) or bddAddressID used
|
||||
// in tests (in format [bddAddressID]) and returns always the real address ID.
|
||||
// If the address is not found, the ID of main address is returned.
|
||||
func (a *TestAccount) EnsureAddressID(addressOrAddressTestID string) string {
|
||||
if strings.HasPrefix(addressOrAddressTestID, "[") {
|
||||
addressTestID := addressOrAddressTestID[1 : len(addressOrAddressTestID)-1]
|
||||
address := a.addressesByBDDAddressID[addressTestID]
|
||||
return address.ID
|
||||
}
|
||||
for _, address := range a.addressesByBDDAddressID {
|
||||
if address.Email == addressOrAddressTestID {
|
||||
return address.ID
|
||||
}
|
||||
}
|
||||
return a.AddressID()
|
||||
}
|
||||
|
||||
// EnsureAddress accepts address (simply the address) or bddAddressID used
|
||||
// in tests (in format [bddAddressID]) and returns always the address.
|
||||
// If the address ID cannot be found, the original value is returned.
|
||||
func (a *TestAccount) EnsureAddress(addressOrAddressTestID string) string {
|
||||
if strings.HasPrefix(addressOrAddressTestID, "[") {
|
||||
addressTestID := addressOrAddressTestID[1 : len(addressOrAddressTestID)-1]
|
||||
address := a.addressesByBDDAddressID[addressTestID]
|
||||
return address.Email
|
||||
}
|
||||
return addressOrAddressTestID
|
||||
}
|
||||
|
||||
func (a *TestAccount) Password() string {
|
||||
return a.password
|
||||
}
|
||||
|
||||
func (a *TestAccount) MailboxPassword() string {
|
||||
return a.mailboxPassword
|
||||
}
|
||||
|
||||
func (a *TestAccount) IsTwoFAEnabled() bool {
|
||||
return a.twoFAEnabled
|
||||
}
|
||||
|
||||
func (a *TestAccount) BridgePassword() string {
|
||||
return BridgePassword
|
||||
}
|
||||
79
test/accounts/accounts.go
Normal file
79
test/accounts/accounts.go
Normal file
@ -0,0 +1,79 @@
|
||||
// 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 accounts
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// BridgePassword is password to be used for IMAP or SMTP under tests.
|
||||
const BridgePassword = "bridgepassword"
|
||||
|
||||
type TestAccounts struct {
|
||||
Users map[string]*pmapi.User // Key is user ID used in BDD.
|
||||
Addresses map[string]map[string]*pmapi.Address // Key is real user ID, second key is address ID used in BDD.
|
||||
Passwords map[string]string // Key is real user ID.
|
||||
MailboxPasswords map[string]string // Key is real user ID.
|
||||
TwoFAs map[string]bool // Key is real user ID.
|
||||
}
|
||||
|
||||
func Load(path string) (*TestAccounts, error) {
|
||||
data, err := ioutil.ReadFile(path) //nolint[gosec]
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to load JSON")
|
||||
}
|
||||
|
||||
var testAccounts TestAccounts
|
||||
err = json.Unmarshal(data, &testAccounts)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal JSON")
|
||||
}
|
||||
|
||||
return &testAccounts, nil
|
||||
}
|
||||
|
||||
func (a *TestAccounts) GetTestAccount(username string) *TestAccount {
|
||||
return a.GetTestAccountWithAddress(username, "")
|
||||
}
|
||||
|
||||
func (a *TestAccounts) GetTestAccountWithAddress(username, addressID string) *TestAccount {
|
||||
// Do lookup by full address and convert to name in tests.
|
||||
// Used by getting real data to ensure correct address or address ID.
|
||||
for key, user := range a.Users {
|
||||
if user.Name == username {
|
||||
username = key
|
||||
break
|
||||
}
|
||||
}
|
||||
user, ok := a.Users[username]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return newTestAccount(
|
||||
user,
|
||||
a.Addresses[user.Name],
|
||||
addressID,
|
||||
a.Passwords[user.Name],
|
||||
a.MailboxPasswords[user.Name],
|
||||
a.TwoFAs[user.Name],
|
||||
)
|
||||
}
|
||||
105
test/accounts/fake.json
Normal file
105
test/accounts/fake.json
Normal file
@ -0,0 +1,105 @@
|
||||
{
|
||||
"users": {
|
||||
"user": {
|
||||
"ID": "1",
|
||||
"Name": "user"
|
||||
},
|
||||
"user2fa": {
|
||||
"ID": "2",
|
||||
"Name": "user2fa"
|
||||
},
|
||||
"userAddressWithCapitalLetter": {
|
||||
"ID": "3",
|
||||
"Name": "userAddressWithCapitalLetter"
|
||||
},
|
||||
"userMoreAddresses": {
|
||||
"ID": "4",
|
||||
"Name": "userMoreAddresses"
|
||||
},
|
||||
"userDisabledPrimaryAddress": {
|
||||
"ID": "5",
|
||||
"Name": "userDisabledPrimaryAddress"
|
||||
}
|
||||
},
|
||||
"addresses": {
|
||||
"user": {
|
||||
"userAddress": {
|
||||
"ID": "userAddress",
|
||||
"Email": "user@pm.me",
|
||||
"Order": 1,
|
||||
"Receive": 1
|
||||
}
|
||||
},
|
||||
"user2fa": {
|
||||
"user2faAddress": {
|
||||
"ID": "user2faAddress",
|
||||
"Email": "user@pm.me",
|
||||
"Order": 1,
|
||||
"Receive": 1
|
||||
}
|
||||
},
|
||||
"userAddressWithCapitalLetter": {
|
||||
"userAddressWithCapitalLetterAddress": {
|
||||
"ID": "userAddressWithCapitalLetterAddress",
|
||||
"Email": "uSeR@pm.me",
|
||||
"Order": 1,
|
||||
"Receive": 1
|
||||
}
|
||||
},
|
||||
"userMoreAddresses": {
|
||||
"primary": {
|
||||
"ID": "primary",
|
||||
"Email": "primaryaddress@pm.me",
|
||||
"Order": 1,
|
||||
"Receive": 1
|
||||
},
|
||||
"secondary": {
|
||||
"ID": "secondary",
|
||||
"Email": "secondaryaddress@pm.me",
|
||||
"Order": 2,
|
||||
"Receive": 1
|
||||
},
|
||||
"disabled": {
|
||||
"ID": "disabled",
|
||||
"Email": "disabledaddress@pm.me",
|
||||
"Order": 3,
|
||||
"Receive": 0
|
||||
}
|
||||
},
|
||||
"userDisabledPrimaryAddress": {
|
||||
"primary": {
|
||||
"ID": "primary",
|
||||
"Email": "user@pm.me",
|
||||
"Order": 1,
|
||||
"Receive": 0
|
||||
},
|
||||
"secondary": {
|
||||
"ID": "secondary",
|
||||
"Email": "user@pm.me",
|
||||
"Order": 2,
|
||||
"Receive": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"passwords": {
|
||||
"user": "password",
|
||||
"user2fa": "password",
|
||||
"userAddressWithCapitalLetter": "password",
|
||||
"userMoreAddresses": "password",
|
||||
"userDisabledPrimaryAddress": "password"
|
||||
},
|
||||
"mailboxPasswords": {
|
||||
"user": "password",
|
||||
"user2fa": "password",
|
||||
"userAddressWithCapitalLetter": "password",
|
||||
"userMoreAddresses": "password",
|
||||
"userDisabledPrimaryAddress": "password"
|
||||
},
|
||||
"twoFAs": {
|
||||
"user": false,
|
||||
"user2fa": true,
|
||||
"userAddressWithCapitalLetter": false,
|
||||
"userMoreAddresses": false,
|
||||
"userDisabledPrimaryAddress": false
|
||||
}
|
||||
}
|
||||
79
test/api_checks_test.go
Normal file
79
test/api_checks_test.go
Normal file
@ -0,0 +1,79 @@
|
||||
// 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 tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/cucumber/godog"
|
||||
"github.com/cucumber/godog/gherkin"
|
||||
)
|
||||
|
||||
func APIChecksFeatureContext(s *godog.Suite) {
|
||||
s.Step(`^API endpoint "([^"]*)" is called with:$`, apiIsCalledWith)
|
||||
s.Step(`^message is sent with API call:$`, messageIsSentWithAPICall)
|
||||
}
|
||||
|
||||
func apiIsCalledWith(endpoint string, data *gherkin.DocString) error {
|
||||
split := strings.Split(endpoint, " ")
|
||||
method := split[0]
|
||||
path := split[1]
|
||||
request := []byte(data.Content)
|
||||
if !ctx.GetPMAPIController().WasCalled(method, path, request) {
|
||||
return fmt.Errorf("%s was not called with %s", endpoint, request)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func messageIsSentWithAPICall(data *gherkin.DocString) error {
|
||||
endpoint := "POST /messages"
|
||||
if err := apiIsCalledWith(endpoint, data); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, request := range ctx.GetPMAPIController().GetCalls("POST", "/messages") {
|
||||
if !checkAllRequiredFieldsForSendingMessage(request) {
|
||||
return fmt.Errorf("%s was not called with all required fields: %s", endpoint, request)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkAllRequiredFieldsForSendingMessage(request []byte) bool {
|
||||
if matches := regexp.MustCompile(`"Subject":`).Match(request); !matches {
|
||||
return false
|
||||
}
|
||||
if matches := regexp.MustCompile(`"ToList":`).Match(request); !matches {
|
||||
return false
|
||||
}
|
||||
if matches := regexp.MustCompile(`"CCList":`).Match(request); !matches {
|
||||
return false
|
||||
}
|
||||
if matches := regexp.MustCompile(`"BCCList":`).Match(request); !matches {
|
||||
return false
|
||||
}
|
||||
if matches := regexp.MustCompile(`"AddressID":`).Match(request); !matches {
|
||||
return false
|
||||
}
|
||||
if matches := regexp.MustCompile(`"Body":`).Match(request); !matches {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
66
test/bdd_test.go
Normal file
66
test/bdd_test.go
Normal file
@ -0,0 +1,66 @@
|
||||
// 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 tests
|
||||
|
||||
import (
|
||||
"github.com/ProtonMail/proton-bridge/test/context"
|
||||
"github.com/cucumber/godog"
|
||||
)
|
||||
|
||||
func FeatureContext(s *godog.Suite) {
|
||||
s.BeforeScenario(beforeScenario)
|
||||
s.AfterScenario(afterScenario)
|
||||
|
||||
APIChecksFeatureContext(s)
|
||||
|
||||
BridgeActionsFeatureContext(s)
|
||||
BridgeChecksFeatureContext(s)
|
||||
BridgeSetupFeatureContext(s)
|
||||
|
||||
IMAPActionsAuthFeatureContext(s)
|
||||
IMAPActionsMailboxFeatureContext(s)
|
||||
IMAPActionsMessagesFeatureContext(s)
|
||||
IMAPChecksFeatureContext(s)
|
||||
IMAPSetupFeatureContext(s)
|
||||
|
||||
SMTPActionsAuthFeatureContext(s)
|
||||
SMTPChecksFeatureContext(s)
|
||||
SMTPSetupFeatureContext(s)
|
||||
|
||||
StoreActionsFeatureContext(s)
|
||||
StoreChecksFeatureContext(s)
|
||||
StoreSetupFeatureContext(s)
|
||||
}
|
||||
|
||||
var ctx *context.TestContext //nolint[gochecknoglobals]
|
||||
|
||||
func beforeScenario(scenario interface{}) {
|
||||
ctx = context.New()
|
||||
}
|
||||
|
||||
func afterScenario(scenario interface{}, err error) {
|
||||
if err != nil {
|
||||
for _, user := range ctx.GetBridge().GetUsers() {
|
||||
user.GetStore().TestDumpDB(ctx.GetTestingT())
|
||||
}
|
||||
}
|
||||
ctx.Cleanup()
|
||||
if err != nil {
|
||||
ctx.GetPMAPIController().PrintCalls()
|
||||
}
|
||||
}
|
||||
104
test/benchmarks/bench_results/human-table.py
Normal file
104
test/benchmarks/bench_results/human-table.py
Normal file
@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import glob
|
||||
import pandas as pd
|
||||
import re
|
||||
|
||||
|
||||
def print_header(report_file):
|
||||
print('\n======== %s ========' %
|
||||
(report_file.replace("./bench-", "").replace(".log", "")))
|
||||
|
||||
|
||||
rx_line = {
|
||||
'exists': re.compile(r'.*Res[A-Za-z]?: [*] (?P<mails>\d+) EXISTS.*\n'),
|
||||
'bench': re.compile(r'Benchmark(?P<name>[^ \t]+)[ \t]+(?P<rpts>\d+)[ \t]+(?P<ns>\d+) ns/op.*\n'),
|
||||
# 'total' : re.compile(r'ok[ \t]+(?P<pkg>[^ \t]+)[ \t]+(?P<time>[^ \t\n]+)[ \t]*\n'),
|
||||
}
|
||||
|
||||
|
||||
def parse_line(line):
|
||||
for key, rx in rx_line.items():
|
||||
match = rx.search(line)
|
||||
if match:
|
||||
return key, match
|
||||
# if there are no matches
|
||||
return None, None
|
||||
|
||||
|
||||
rx_count = re.compile(r'Fetch/1:(?P<count>\d+)-')
|
||||
|
||||
|
||||
def parse_file(filepath):
|
||||
data = [] # create an empty list to collect the data
|
||||
# open the file and read through it line by line
|
||||
with open(filepath, 'r') as file_object:
|
||||
line = file_object.readline()
|
||||
last_count = 0
|
||||
mails = 1
|
||||
while line:
|
||||
# at each line check for a match with a regex
|
||||
key, match = parse_line(line)
|
||||
# print(line, key, match)
|
||||
if key != None:
|
||||
row = match.groupdict()
|
||||
if key == 'exists':
|
||||
mails = int(row['mails'])
|
||||
last_count = 0
|
||||
if key == 'bench':
|
||||
match = rx_count.search(row['name'])
|
||||
row['mails'] = mails - last_count
|
||||
if match:
|
||||
count = int(match.group('count'))
|
||||
if count < mails:
|
||||
row['mails'] = count - last_count
|
||||
last_count = count
|
||||
row['rpts'] = int(row['rpts'])
|
||||
row['ns'] = int(row['ns'])
|
||||
row['time/op'] = human_duration(row['ns'])
|
||||
if row['mails'] > 0:
|
||||
row['time/mails'] = human_duration(
|
||||
row['ns']/row['mails']
|
||||
)
|
||||
data.append(row)
|
||||
if key == 'total':
|
||||
row['name'] = key
|
||||
data.append(row)
|
||||
line = file_object.readline()
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def human_duration(duration_ns):
|
||||
unit = 'ns'
|
||||
factor = 1.
|
||||
unit_factors = [
|
||||
('us', 1.e3),
|
||||
('ms', 1.e3),
|
||||
('s ', 1.e3),
|
||||
('m ', 60.),
|
||||
('h ', 60.),
|
||||
('d ', 24.),
|
||||
('w ', 7.),
|
||||
('m ', 30./7.),
|
||||
('y ', 12.),
|
||||
]
|
||||
for unit_factor in unit_factors:
|
||||
if (abs(duration_ns) / factor / unit_factor[1]) < 1.0:
|
||||
break
|
||||
unit = unit_factor[0]
|
||||
factor *= unit_factor[1]
|
||||
return "%4.2f%s" % (duration_ns/factor, unit)
|
||||
|
||||
|
||||
def print_table(data):
|
||||
data = pd.DataFrame(data)
|
||||
data.set_index('name', inplace=True)
|
||||
print(data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# for d in [ 0.5, 1, 2, 5, 1e3, 5e3, 1e4, 1e5, 1e6, 1e9, 2e9, 1e10, 1e11, 1e12, ]: print(human_duration(int(d)))
|
||||
for report_file in glob.glob("./*.log"):
|
||||
print_header(report_file)
|
||||
print_table(parse_file(report_file))
|
||||
201
test/benchmarks/bench_test.go
Normal file
201
test/benchmarks/bench_test.go
Normal file
@ -0,0 +1,201 @@
|
||||
// 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 benchmarks
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/test/context"
|
||||
"github.com/ProtonMail/proton-bridge/test/mocks"
|
||||
)
|
||||
|
||||
func benchTestContext() (*context.TestContext, *mocks.IMAPClient) {
|
||||
ctx := context.New()
|
||||
|
||||
username := "user"
|
||||
account := ctx.GetTestAccount(username)
|
||||
if account == nil {
|
||||
panic("account " + username + " does not exist")
|
||||
}
|
||||
|
||||
_ = ctx.GetPMAPIController().AddUser(account.User(), account.Addresses(), account.Password(), account.IsTwoFAEnabled())
|
||||
if err := ctx.LoginUser(account.Username(), account.Password(), account.MailboxPassword()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
imapClient := ctx.GetIMAPClient("client")
|
||||
imapClient.Login(account.Address(), account.BridgePassword())
|
||||
|
||||
// waitForSync between bridge and API. There is no way to know precisely
|
||||
// from the outside when the bridge is synced. We could wait for first
|
||||
// response from any fetch, but we don't know how many messages should be
|
||||
// there. Unless we hard code the number of messages.
|
||||
// Please, check this time is enough when doing benchmarks and don't forget
|
||||
// to exclude this time from total time.
|
||||
time.Sleep(10 * time.Second)
|
||||
|
||||
return ctx, imapClient
|
||||
}
|
||||
|
||||
func BenchmarkIMAPFetch(b *testing.B) {
|
||||
tc, c := benchTestContext()
|
||||
defer tc.Cleanup()
|
||||
|
||||
c.Select("All Mail").AssertOK()
|
||||
|
||||
fetchBench := []struct{ ids, args string }{
|
||||
{"1:10", "rfc822.size"},
|
||||
{"1:100", "rfc822.size"},
|
||||
{"1:1000", "rfc822.size"},
|
||||
{"1:*", "rfc822.size"},
|
||||
}
|
||||
|
||||
for _, bd := range fetchBench {
|
||||
ids, args := bd.ids, bd.args // pin
|
||||
b.Run(ids+"-"+args, func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
c.Fetch(ids, args)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCachingFetch(b *testing.B) {
|
||||
tc, c := benchTestContext()
|
||||
defer tc.Cleanup()
|
||||
|
||||
c.Select("\"All Mail\"").AssertOK()
|
||||
|
||||
ids := "1:100"
|
||||
args := "body.peek[]"
|
||||
tries := []string{"long", "short"}
|
||||
|
||||
for _, try := range tries {
|
||||
b.Run(strings.Join([]string{ids, args, try}, "-"), func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
c.Fetch(ids, args)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkIMAPAppleMail(b *testing.B) {
|
||||
tc, c := benchTestContext()
|
||||
defer tc.Cleanup()
|
||||
|
||||
// assume we have at least 50 messages in INBOX
|
||||
idRange := "1:50"
|
||||
newUID := "50" // assume that Apple mail don't know about this mail
|
||||
|
||||
// I will use raw send command to completely reproduce the calls
|
||||
// (including quotation and case sensitivity)
|
||||
b.Run("default", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, command := range []string{
|
||||
"CAPABILITY",
|
||||
"ID (" +
|
||||
`"name" "Mac OS X Mail" ` +
|
||||
`"version" "11.5 (3445.9.1)" ` +
|
||||
`"os" "Mac OS X" ` +
|
||||
`"os-version" "10.13.6 (17G3025)" ` +
|
||||
`"vendor" "Apple Inc."` +
|
||||
")",
|
||||
`LIST "" ""`,
|
||||
`STATUS INBOX (MESSAGES UIDNEXT UIDVALIDITY UNSEEN)`,
|
||||
`SELECT INBOX`,
|
||||
`FETCH ` + idRange + ` (FLAGS UID)`,
|
||||
`FETCH ` + idRange + " " +
|
||||
`(` +
|
||||
`INTERNALDATE UID RFC822.SIZE FLAGS ` +
|
||||
`BODY.PEEK[` +
|
||||
`HEADER.FIELDS (` +
|
||||
`date subject from to cc message-id in-reply-to references ` +
|
||||
`x-priority x-uniform-type-identifier x-universally-unique-identifier ` +
|
||||
`list-id list-unsubscribe` +
|
||||
`)])`,
|
||||
`UID FETCH ` + newUID + ` (BODYSTRUCTURE BODY.PEEK[HEADER])`,
|
||||
// if email has attachment it is splitted to several fetches
|
||||
//`UID FETCH 133 (BODY.PEEK[3]<0.5877469> BODY.PEEK[1] BODY.PEEK[2])`,
|
||||
//`UID FETCH 133 BODY.PEEK[3]<5877469.2925661>`,
|
||||
// here I will just use section download, which is used by AppleMail
|
||||
`UID FETCH ` + newUID + ` BODY.PEEK[1]`,
|
||||
// here I will just use partial download, which is used by AppleMail
|
||||
`UID FETCH ` + newUID + ` BODY.PEEK[]<0.2000>`,
|
||||
} {
|
||||
c.SendCommand(command).AssertOK()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkIMAPOutlook(b *testing.B) {
|
||||
tc, c := benchTestContext()
|
||||
defer tc.Cleanup()
|
||||
|
||||
// assume we have at least 50 messages in INBOX
|
||||
idRange := "1:50"
|
||||
|
||||
// I will use raw send command to completely reproduce the calls
|
||||
// (including quotation and case sensitivity)
|
||||
b.Run("default", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, command := range []string{
|
||||
|
||||
/*
|
||||
"ID ("+
|
||||
`"name" "Mac OS X Mail" `+
|
||||
`"version" "11.5 (3445.9.1)" `+
|
||||
`"os" "Mac OS X" `+
|
||||
`"os-version" "10.13.6 (17G3025)" `+
|
||||
`"vendor" "Apple Inc."`+
|
||||
")",
|
||||
*/
|
||||
|
||||
`SELECT "INBOX"`,
|
||||
`UID SEARCH ` + idRange + ` SINCE 01-Sep-2019`,
|
||||
`UID FETCH 1:* (UID FLAGS)`,
|
||||
`UID FETCH ` + idRange + ` (UID FLAGS RFC822.SIZE BODY.PEEK[] INTERNALDATE)`,
|
||||
} {
|
||||
c.SendCommand(command).AssertOK()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkIMAPThunderbird(b *testing.B) {
|
||||
tc, c := benchTestContext()
|
||||
defer tc.Cleanup()
|
||||
|
||||
// I will use raw send command to completely reproduce the calls
|
||||
// (including quotation and case sensitivity)
|
||||
b.Run("default", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, command := range []string{
|
||||
`capability`,
|
||||
`ID ("name" "Thunderbird" "version" "68.2.0")`,
|
||||
`select "INBOX"`,
|
||||
`getquotaroot "INBOX"`,
|
||||
`UID fetch 1:* (FLAGS)`,
|
||||
} {
|
||||
c.SendCommand(command).AssertOK()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
134
test/bridge_actions_test.go
Normal file
134
test/bridge_actions_test.go
Normal file
@ -0,0 +1,134 @@
|
||||
// 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 tests
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/cucumber/godog"
|
||||
)
|
||||
|
||||
func BridgeActionsFeatureContext(s *godog.Suite) {
|
||||
s.Step(`^bridge starts$`, bridgeStarts)
|
||||
s.Step(`^bridge syncs "([^"]*)"$`, bridgeSyncsUser)
|
||||
s.Step(`^"([^"]*)" logs in to bridge$`, userLogsInToBridge)
|
||||
s.Step(`^"([^"]*)" logs in to bridge with bad password$`, userLogsInToBridgeWithBadPassword)
|
||||
s.Step(`^"([^"]*)" logs out from bridge$`, userLogsOutFromBridge)
|
||||
s.Step(`^"([^"]*)" changes the address mode$`, userChangesTheAddressMode)
|
||||
s.Step(`^user deletes "([^"]*)" from bridge$`, userDeletesUserFromBridge)
|
||||
s.Step(`^user deletes "([^"]*)" from bridge with cache$`, userDeletesUserFromBridgeWithCache)
|
||||
s.Step(`^the internet connection is lost$`, theInternetConnectionIsLost)
|
||||
s.Step(`^the internet connection is restored$`, theInternetConnectionIsRestored)
|
||||
s.Step(`^(\d+) seconds pass$`, secondsPass)
|
||||
}
|
||||
|
||||
func bridgeStarts() error {
|
||||
ctx.SetLastBridgeError(ctx.RestartBridge())
|
||||
return nil
|
||||
}
|
||||
|
||||
func bridgeSyncsUser(bddUserID string) error {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
if err := ctx.WaitForSync(account.Username()); err != nil {
|
||||
return internalError(err, "waiting for sync")
|
||||
}
|
||||
ctx.SetLastBridgeError(ctx.GetTestingError())
|
||||
return nil
|
||||
}
|
||||
|
||||
func userLogsInToBridge(bddUserID string) error {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
ctx.SetLastBridgeError(ctx.LoginUser(account.Username(), account.Password(), account.MailboxPassword()))
|
||||
return nil
|
||||
}
|
||||
|
||||
func userLogsInToBridgeWithBadPassword(bddUserID string) error {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
ctx.SetLastBridgeError(ctx.LoginUser(account.Username(), "you shall not pass!", "123"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func userLogsOutFromBridge(bddUserID string) error {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
ctx.SetLastBridgeError(ctx.LogoutUser(account.Username()))
|
||||
return nil
|
||||
}
|
||||
|
||||
func userChangesTheAddressMode(bddUserID string) error {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
bridgeUser, err := ctx.GetUser(account.Username())
|
||||
if err != nil {
|
||||
return internalError(err, "getting user %s", account.Username())
|
||||
}
|
||||
if err := bridgeUser.SwitchAddressMode(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.EventuallySyncIsFinishedForUsername(account.Username())
|
||||
return nil
|
||||
}
|
||||
|
||||
func userDeletesUserFromBridge(bddUserID string) error {
|
||||
return deleteUserFromBridge(bddUserID, false)
|
||||
}
|
||||
|
||||
func userDeletesUserFromBridgeWithCache(bddUserID string) error {
|
||||
return deleteUserFromBridge(bddUserID, true)
|
||||
}
|
||||
|
||||
func deleteUserFromBridge(bddUserID string, cache bool) error {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
bridgeUser, err := ctx.GetUser(account.Username())
|
||||
if err != nil {
|
||||
return internalError(err, "getting user %s", account.Username())
|
||||
}
|
||||
return ctx.GetBridge().DeleteUser(bridgeUser.ID(), cache)
|
||||
}
|
||||
|
||||
func theInternetConnectionIsLost() error {
|
||||
ctx.GetPMAPIController().TurnInternetConnectionOff()
|
||||
return nil
|
||||
}
|
||||
|
||||
func theInternetConnectionIsRestored() error {
|
||||
ctx.GetPMAPIController().TurnInternetConnectionOn()
|
||||
return nil
|
||||
}
|
||||
|
||||
func secondsPass(seconds int) error {
|
||||
time.Sleep(time.Duration(seconds) * time.Second)
|
||||
return nil
|
||||
}
|
||||
198
test/bridge_checks_test.go
Normal file
198
test/bridge_checks_test.go
Normal file
@ -0,0 +1,198 @@
|
||||
// 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 tests
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/cucumber/godog"
|
||||
a "github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func BridgeChecksFeatureContext(s *godog.Suite) {
|
||||
s.Step(`^bridge response is "([^"]*)"$`, bridgeResponseIs)
|
||||
s.Step(`^"([^"]*)" has address mode in "([^"]*)" mode$`, userHasAddressModeInMode)
|
||||
s.Step(`^"([^"]*)" is disconnected$`, userIsDisconnected)
|
||||
s.Step(`^"([^"]*)" is connected$`, userIsConnected)
|
||||
s.Step(`^"([^"]*)" has database file$`, userHasDatabaseFile)
|
||||
s.Step(`^"([^"]*)" does not have database file$`, userDoesNotHaveDatabaseFile)
|
||||
s.Step(`^"([^"]*)" has loaded store$`, userHasLoadedStore)
|
||||
s.Step(`^"([^"]*)" does not have loaded store$`, userDoesNotHaveLoadedStore)
|
||||
s.Step(`^"([^"]*)" has running event loop$`, userHasRunningEventLoop)
|
||||
s.Step(`^"([^"]*)" does not have running event loop$`, userDoesNotHaveRunningEventLoop)
|
||||
s.Step(`^"([^"]*)" does not have API auth$`, doesNotHaveAPIAuth)
|
||||
s.Step(`^"([^"]*)" has API auth$`, hasAPIAuth)
|
||||
}
|
||||
|
||||
func bridgeResponseIs(expectedResponse string) error {
|
||||
err := ctx.GetLastBridgeError()
|
||||
if expectedResponse == "OK" {
|
||||
a.NoError(ctx.GetTestingT(), err)
|
||||
} else {
|
||||
a.EqualError(ctx.GetTestingT(), err, expectedResponse)
|
||||
}
|
||||
return ctx.GetTestingError()
|
||||
}
|
||||
|
||||
func userHasAddressModeInMode(bddUserID, wantAddressMode string) error {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
bridgeUser, err := ctx.GetUser(account.Username())
|
||||
if err != nil {
|
||||
return internalError(err, "getting user %s", account.Username())
|
||||
}
|
||||
addressMode := "split"
|
||||
if bridgeUser.IsCombinedAddressMode() {
|
||||
addressMode = "combined"
|
||||
}
|
||||
a.Equal(ctx.GetTestingT(), wantAddressMode, addressMode)
|
||||
return ctx.GetTestingError()
|
||||
}
|
||||
|
||||
func userIsDisconnected(bddUserID string) error {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
bridgeUser, err := ctx.GetUser(account.Username())
|
||||
if err != nil {
|
||||
return internalError(err, "getting user %s", account.Username())
|
||||
}
|
||||
a.False(ctx.GetTestingT(), bridgeUser.IsConnected())
|
||||
return ctx.GetTestingError()
|
||||
}
|
||||
|
||||
func userIsConnected(bddUserID string) error {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
t := ctx.GetTestingT()
|
||||
bridgeUser, err := ctx.GetUser(account.Username())
|
||||
if err != nil {
|
||||
return internalError(err, "getting user %s", account.Username())
|
||||
}
|
||||
a.True(ctx.GetTestingT(), bridgeUser.IsConnected())
|
||||
a.NotEmpty(t, bridgeUser.GetPrimaryAddress())
|
||||
a.NotEmpty(t, bridgeUser.GetStoreAddresses())
|
||||
return ctx.GetTestingError()
|
||||
}
|
||||
|
||||
func userHasDatabaseFile(bddUserID string) error {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
filePath := ctx.GetDatabaseFilePath(account.UserID())
|
||||
a.FileExists(ctx.GetTestingT(), filePath)
|
||||
return ctx.GetTestingError()
|
||||
}
|
||||
|
||||
func userDoesNotHaveDatabaseFile(bddUserID string) error {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
filePath := ctx.GetDatabaseFilePath(account.UserID())
|
||||
a.NoFileExists(ctx.GetTestingT(), filePath)
|
||||
return ctx.GetTestingError()
|
||||
}
|
||||
|
||||
func userHasLoadedStore(bddUserID string) error {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
bridgeUser, err := ctx.GetUser(account.Username())
|
||||
if err != nil {
|
||||
return internalError(err, "getting user %s", account.Username())
|
||||
}
|
||||
a.NotNil(ctx.GetTestingT(), bridgeUser.GetStore())
|
||||
return ctx.GetTestingError()
|
||||
}
|
||||
|
||||
func userDoesNotHaveLoadedStore(bddUserID string) error {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
bridgeUser, err := ctx.GetUser(account.Username())
|
||||
if err != nil {
|
||||
return internalError(err, "getting user %s", account.Username())
|
||||
}
|
||||
a.Nil(ctx.GetTestingT(), bridgeUser.GetStore())
|
||||
return ctx.GetTestingError()
|
||||
}
|
||||
|
||||
func userHasRunningEventLoop(bddUserID string) error {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
store, err := ctx.GetStore(account.Username())
|
||||
if err != nil {
|
||||
return internalError(err, "getting store of %s", account.Username())
|
||||
}
|
||||
a.Eventually(ctx.GetTestingT(), func() bool {
|
||||
return store.TestGetEventLoop() != nil && store.TestGetEventLoop().IsRunning()
|
||||
}, 5*time.Second, 10*time.Millisecond)
|
||||
return ctx.GetTestingError()
|
||||
}
|
||||
|
||||
func userDoesNotHaveRunningEventLoop(bddUserID string) error {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
store, err := ctx.GetStore(account.Username())
|
||||
if err != nil {
|
||||
return internalError(err, "getting store of %s", account.Username())
|
||||
}
|
||||
a.Eventually(ctx.GetTestingT(), func() bool {
|
||||
return store.TestGetEventLoop() == nil || !store.TestGetEventLoop().IsRunning()
|
||||
}, 5*time.Second, 10*time.Millisecond)
|
||||
return ctx.GetTestingError()
|
||||
}
|
||||
|
||||
func hasAPIAuth(accountName string) error {
|
||||
account := ctx.GetTestAccount(accountName)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
bridgeUser, err := ctx.GetUser(account.Username())
|
||||
if err != nil {
|
||||
return internalError(err, "getting user %s", account.Username())
|
||||
}
|
||||
a.True(ctx.GetTestingT(), bridgeUser.HasAPIAuth())
|
||||
return ctx.GetTestingError()
|
||||
}
|
||||
|
||||
func doesNotHaveAPIAuth(accountName string) error {
|
||||
account := ctx.GetTestAccount(accountName)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
bridgeUser, err := ctx.GetUser(account.Username())
|
||||
if err != nil {
|
||||
return internalError(err, "getting user %s", account.Username())
|
||||
}
|
||||
a.False(ctx.GetTestingT(), bridgeUser.HasAPIAuth())
|
||||
return ctx.GetTestingError()
|
||||
}
|
||||
139
test/bridge_setup_test.go
Normal file
139
test/bridge_setup_test.go
Normal file
@ -0,0 +1,139 @@
|
||||
// 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 tests
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/cucumber/godog"
|
||||
a "github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func BridgeSetupFeatureContext(s *godog.Suite) {
|
||||
s.Step(`^there is no internet connection$`, thereIsNoInternetConnection)
|
||||
s.Step(`^there is user "([^"]*)"$`, thereIsUser)
|
||||
s.Step(`^there is connected user "([^"]*)"$`, thereIsConnectedUser)
|
||||
s.Step(`^there is disconnected user "([^"]*)"$`, thereIsDisconnectedUser)
|
||||
s.Step(`^there is database file for "([^"]*)"$`, thereIsDatabaseFileForUser)
|
||||
s.Step(`^there is no database file for "([^"]*)"$`, thereIsNoDatabaseFileForUser)
|
||||
s.Step(`^there is "([^"]*)" in "([^"]*)" address mode$`, thereIsUserWithAddressMode)
|
||||
}
|
||||
|
||||
func thereIsNoInternetConnection() error {
|
||||
ctx.GetPMAPIController().TurnInternetConnectionOff()
|
||||
return nil
|
||||
}
|
||||
|
||||
func thereIsUser(bddUserID string) error {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
err := ctx.GetPMAPIController().AddUser(account.User(), account.Addresses(), account.Password(), account.IsTwoFAEnabled())
|
||||
return internalError(err, "adding user %s", account.Username())
|
||||
}
|
||||
|
||||
func thereIsConnectedUser(bddUserID string) error {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
err := ctx.GetPMAPIController().AddUser(account.User(), account.Addresses(), account.Password(), account.IsTwoFAEnabled())
|
||||
if err != nil {
|
||||
return internalError(err, "adding user %s", account.Username())
|
||||
}
|
||||
return ctx.LoginUser(account.Username(), account.Password(), account.MailboxPassword())
|
||||
}
|
||||
|
||||
func thereIsDisconnectedUser(bddUserID string) error {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
err := ctx.GetPMAPIController().AddUser(account.User(), account.Addresses(), account.Password(), account.IsTwoFAEnabled())
|
||||
if err != nil {
|
||||
return internalError(err, "adding user %s", account.Username())
|
||||
}
|
||||
err = ctx.LoginUser(account.Username(), account.Password(), account.MailboxPassword())
|
||||
if err != nil {
|
||||
return internalError(err, "logging user %s in", account.Username())
|
||||
}
|
||||
user, err := ctx.GetUser(account.Username())
|
||||
if err != nil {
|
||||
return internalError(err, "getting user %s", account.Username())
|
||||
}
|
||||
err = user.Logout()
|
||||
if err != nil {
|
||||
return internalError(err, "disconnecting user %s", account.Username())
|
||||
}
|
||||
|
||||
// We need to wait till event loop is stopped because when it's stopped
|
||||
// logout is also called and if we would do login at the same time, it
|
||||
// wouldn't work. 100 ms after event loop is stopped should be enough.
|
||||
a.Eventually(ctx.GetTestingT(), func() bool {
|
||||
return !user.GetStore().TestGetEventLoop().IsRunning()
|
||||
}, 1*time.Second, 10*time.Millisecond)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
return ctx.GetTestingError()
|
||||
}
|
||||
|
||||
func thereIsDatabaseFileForUser(bddUserID string) error {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
filePath := ctx.GetDatabaseFilePath(account.UserID())
|
||||
_, err := os.Stat(filePath)
|
||||
return internalError(err, "getting database file of %s", account.Username())
|
||||
}
|
||||
|
||||
func thereIsNoDatabaseFileForUser(bddUserID string) error {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
filePath := ctx.GetDatabaseFilePath(account.UserID())
|
||||
if _, err := os.Stat(filePath); err != nil {
|
||||
return nil
|
||||
}
|
||||
return internalError(os.Remove(filePath), "removing database file of %s", account.Username())
|
||||
}
|
||||
|
||||
func thereIsUserWithAddressMode(bddUserID, wantAddressMode string) error {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
bridgeUser, err := ctx.GetUser(account.Username())
|
||||
if err != nil {
|
||||
return internalError(err, "getting user %s", account.Username())
|
||||
}
|
||||
addressMode := "split"
|
||||
if bridgeUser.IsCombinedAddressMode() {
|
||||
addressMode = "combined"
|
||||
}
|
||||
if wantAddressMode != addressMode {
|
||||
err := bridgeUser.SwitchAddressMode()
|
||||
if err != nil {
|
||||
return internalError(err, "switching mode")
|
||||
}
|
||||
}
|
||||
ctx.EventuallySyncIsFinishedForUsername(bridgeUser.Username())
|
||||
return nil
|
||||
}
|
||||
57
test/context/accounts.go
Normal file
57
test/context/accounts.go
Normal file
@ -0,0 +1,57 @@
|
||||
// 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 context
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/test/accounts"
|
||||
)
|
||||
|
||||
func newTestAccounts() *accounts.TestAccounts {
|
||||
envFile := os.Getenv("TEST_ACCOUNTS")
|
||||
data, err := accounts.Load(envFile)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func (ctx *TestContext) GetTestAccount(bddUserID string) *accounts.TestAccount {
|
||||
return ctx.testAccounts.GetTestAccount(bddUserID)
|
||||
}
|
||||
|
||||
func (ctx *TestContext) GetTestAccountWithAddress(bddUserID, addressID string) *accounts.TestAccount {
|
||||
return ctx.testAccounts.GetTestAccountWithAddress(bddUserID, addressID)
|
||||
}
|
||||
|
||||
func (ctx *TestContext) EnsureAddressID(bddUserID, addressOrAddressTestID string) string {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return addressOrAddressTestID
|
||||
}
|
||||
return account.EnsureAddressID(addressOrAddressTestID)
|
||||
}
|
||||
|
||||
func (ctx *TestContext) EnsureAddress(bddUserID, addressOrAddressTestID string) string {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return addressOrAddressTestID
|
||||
}
|
||||
return account.EnsureAddress(addressOrAddressTestID)
|
||||
}
|
||||
42
test/context/bddt.go
Normal file
42
test/context/bddt.go
Normal 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 context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type bddT struct {
|
||||
err *multierror.Error
|
||||
}
|
||||
|
||||
func (t *bddT) Errorf(msg string, args ...interface{}) {
|
||||
err := fmt.Errorf(msg, args...)
|
||||
t.err = multierror.Append(t.err, err)
|
||||
}
|
||||
|
||||
func (t *bddT) FailNow() {
|
||||
t.err = multierror.Append(t.err, errors.New("failed by calling FailNow"))
|
||||
}
|
||||
|
||||
func (t *bddT) getErrors() error {
|
||||
return t.err.ErrorOrNil()
|
||||
}
|
||||
84
test/context/bridge.go
Normal file
84
test/context/bridge.go
Normal file
@ -0,0 +1,84 @@
|
||||
// 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 context
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
)
|
||||
|
||||
// GetBridge returns bridge instance.
|
||||
func (ctx *TestContext) GetBridge() *bridge.Bridge {
|
||||
return ctx.bridge
|
||||
}
|
||||
|
||||
// withBridgeInstance creates a bridge instance for use in the test.
|
||||
// Every TestContext has this by default and thus this doesn't need to be exported.
|
||||
func (ctx *TestContext) withBridgeInstance() {
|
||||
pmapiFactory := func(userID string) bridge.PMAPIProvider {
|
||||
return ctx.pmapiController.GetClient(userID)
|
||||
}
|
||||
ctx.bridge = newBridgeInstance(ctx.t, ctx.cfg, ctx.credStore, ctx.listener, pmapiFactory)
|
||||
ctx.addCleanupChecked(ctx.bridge.ClearData, "Cleaning bridge data")
|
||||
}
|
||||
|
||||
// RestartBridge closes store for each user and recreates a bridge instance the same way as `withBridgeInstance`.
|
||||
// NOTE: This is a very problematic method. It doesn't stop the goroutines doing the event loop and the sync.
|
||||
// These goroutines can continue to run and can cause problems or unexpected behaviour (especially
|
||||
// regarding authorization, because if an auth fails, it will log out the user).
|
||||
// To truly emulate bridge restart, we need a way to immediately stop those goroutines.
|
||||
// I have added a channel that waits up to one second for the event loop to stop, but that isn't great.
|
||||
func (ctx *TestContext) RestartBridge() error {
|
||||
for _, user := range ctx.bridge.GetUsers() {
|
||||
_ = user.GetStore().Close()
|
||||
}
|
||||
|
||||
ctx.withBridgeInstance()
|
||||
return nil
|
||||
}
|
||||
|
||||
// newBridgeInstance creates a new bridge instance configured to use the given config/credstore.
|
||||
func newBridgeInstance(
|
||||
t *bddT,
|
||||
cfg *fakeConfig,
|
||||
credStore bridge.CredentialsStorer,
|
||||
eventListener listener.Listener,
|
||||
pmapiFactory bridge.PMAPIProviderFactory,
|
||||
) *bridge.Bridge {
|
||||
version := os.Getenv("VERSION")
|
||||
bridge.UpdateCurrentUserAgent(version, runtime.GOOS, "", "")
|
||||
|
||||
panicHandler := &panicHandler{t: t}
|
||||
pref := preferences.New(cfg)
|
||||
|
||||
return bridge.New(cfg, pref, panicHandler, eventListener, version, pmapiFactory, credStore)
|
||||
}
|
||||
|
||||
// SetLastBridgeError sets the last error that occurred while executing a bridge action.
|
||||
func (ctx *TestContext) SetLastBridgeError(err error) {
|
||||
ctx.bridgeLastError = err
|
||||
}
|
||||
|
||||
// GetLastBridgeError returns the last error that occurred while executing a bridge action.
|
||||
func (ctx *TestContext) GetLastBridgeError() error {
|
||||
return ctx.bridgeLastError
|
||||
}
|
||||
49
test/context/bridge_panic_handler.go
Normal file
49
test/context/bridge_panic_handler.go
Normal file
@ -0,0 +1,49 @@
|
||||
// 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 context
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"runtime/pprof"
|
||||
)
|
||||
|
||||
type panicHandler struct {
|
||||
t *bddT
|
||||
}
|
||||
|
||||
func newPanicHandler(t *bddT) *panicHandler {
|
||||
return &panicHandler{
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
// HandlePanic makes the panicHandler implement the panicHandler interface for bridge.
|
||||
func (ph *panicHandler) HandlePanic() {
|
||||
r := recover()
|
||||
if r != nil {
|
||||
ph.t.Errorf("panic: %s", r)
|
||||
|
||||
r := bytes.NewBufferString("")
|
||||
_ = pprof.Lookup("goroutine").WriteTo(r, 2)
|
||||
b, err := ioutil.ReadAll(r)
|
||||
ph.t.Errorf("pprof details: %s %s", err, b)
|
||||
|
||||
ph.t.FailNow()
|
||||
}
|
||||
}
|
||||
148
test/context/bridge_user.go
Normal file
148
test/context/bridge_user.go
Normal file
@ -0,0 +1,148 @@
|
||||
// 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 context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/store"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/srp"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// LoginUser logs in the user with the given username, password, and mailbox password.
|
||||
func (ctx *TestContext) LoginUser(username, password, mailboxPassword string) (err error) {
|
||||
srp.RandReader = rand.New(rand.NewSource(42))
|
||||
|
||||
client, auth, err := ctx.bridge.Login(username, password)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to login")
|
||||
}
|
||||
|
||||
if auth.HasTwoFactor() {
|
||||
if _, err := client.Auth2FA("2fa code", auth); err != nil {
|
||||
return errors.Wrap(err, "failed to login with 2FA")
|
||||
}
|
||||
}
|
||||
|
||||
user, err := ctx.bridge.FinishLogin(client, auth, mailboxPassword)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to finish login")
|
||||
}
|
||||
|
||||
ctx.addCleanupChecked(user.Logout, "Logging out user")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetUser retrieves the bridge user matching the given query string.
|
||||
func (ctx *TestContext) GetUser(username string) (*bridge.User, error) {
|
||||
return ctx.bridge.GetUser(username)
|
||||
}
|
||||
|
||||
// GetStore retrieves the store for given username.
|
||||
func (ctx *TestContext) GetStore(username string) (*store.Store, error) {
|
||||
user, err := ctx.GetUser(username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return user.GetStore(), nil
|
||||
}
|
||||
|
||||
// GetStoreAddress retrieves the store address for given username and addressID.
|
||||
func (ctx *TestContext) GetStoreAddress(username, addressID string) (*store.Address, error) {
|
||||
store, err := ctx.GetStore(username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return store.GetAddress(addressID)
|
||||
}
|
||||
|
||||
// GetStoreMailbox retrieves the store mailbox for given username, address ID and mailbox name.
|
||||
func (ctx *TestContext) GetStoreMailbox(username, addressID, mailboxName string) (*store.Mailbox, error) {
|
||||
address, err := ctx.GetStoreAddress(username, addressID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return address.GetMailbox(mailboxName)
|
||||
}
|
||||
|
||||
// GetDatabaseFilePath returns the file path of the user's store file.
|
||||
func (ctx *TestContext) GetDatabaseFilePath(userID string) string {
|
||||
// We cannot use store to get information because we need to check db file also when user is deleted from bridge.
|
||||
fileName := fmt.Sprintf("mailbox-%v.db", userID)
|
||||
return filepath.Join(ctx.cfg.GetDBDir(), fileName)
|
||||
}
|
||||
|
||||
// WaitForSync waits for sync to be done.
|
||||
func (ctx *TestContext) WaitForSync(username string) error {
|
||||
store, err := ctx.GetStore(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// First wait for ongoing sync to be done before starting and waiting for new one.
|
||||
ctx.eventuallySyncIsFinished(store)
|
||||
store.TestSync()
|
||||
ctx.eventuallySyncIsFinished(store)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ctx *TestContext) eventuallySyncIsFinished(store *store.Store) {
|
||||
assert.Eventually(ctx.t, func() bool { return !store.TestIsSyncRunning() }, 30*time.Second, 10*time.Millisecond)
|
||||
}
|
||||
|
||||
// EventuallySyncIsFinishedForUsername will wait until sync is finished or
|
||||
// deadline is reached see eventuallySyncIsFinished for timing
|
||||
func (ctx *TestContext) EventuallySyncIsFinishedForUsername(username string) {
|
||||
store, err := ctx.GetStore(username)
|
||||
assert.Nil(ctx.t, err)
|
||||
ctx.eventuallySyncIsFinished(store)
|
||||
}
|
||||
|
||||
// LogoutUser logs out the given user.
|
||||
func (ctx *TestContext) LogoutUser(query string) (err error) {
|
||||
user, err := ctx.bridge.GetUser(query)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get user")
|
||||
}
|
||||
|
||||
if err = user.Logout(); err != nil {
|
||||
return errors.Wrap(err, "failed to logout user")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteUser deletes the given user.
|
||||
func (ctx *TestContext) DeleteUser(query string, deleteStore bool) (err error) {
|
||||
user, err := ctx.bridge.GetUser(query)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get user")
|
||||
}
|
||||
|
||||
if err = ctx.bridge.DeleteUser(user.ID(), deleteStore); err != nil {
|
||||
err = errors.Wrap(err, "failed to delete user")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
87
test/context/cleaner.go
Normal file
87
test/context/cleaner.go
Normal file
@ -0,0 +1,87 @@
|
||||
// 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 context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Cleaner is a test step that cleans up some stuff post-test.
|
||||
type Cleaner struct {
|
||||
// file is the filename of the caller.
|
||||
file string
|
||||
// lineNumber is the line number of the caller.
|
||||
lineNumber int
|
||||
// label is a descriptive label of the step being performed.
|
||||
label string
|
||||
// ctx is the TestContext on which the step operates.
|
||||
ctx *TestContext
|
||||
// cleanup is callback doing clean up.
|
||||
cleanup func()
|
||||
}
|
||||
|
||||
// Execute runs the cleaner operation.
|
||||
func (c *Cleaner) Execute() {
|
||||
c.ctx.logger.WithField("from", c.From()).Info(c.label)
|
||||
c.cleanup()
|
||||
}
|
||||
|
||||
// From returns the filepath and line number of the place where this cleaner was created.
|
||||
func (c *Cleaner) From() string {
|
||||
return fmt.Sprintf("%v:%v", c.file, c.lineNumber)
|
||||
}
|
||||
|
||||
// addCleanup adds an operation to be performed at the end of the test.
|
||||
func (ctx *TestContext) addCleanup(c func(), label string) {
|
||||
cleaner := &Cleaner{
|
||||
cleanup: c,
|
||||
label: label,
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
if _, file, line, ok := runtime.Caller(1); ok {
|
||||
cleaner.file, cleaner.lineNumber = filepath.Base(file), line
|
||||
}
|
||||
|
||||
ctx.cleanupSteps = append(ctx.cleanupSteps, cleaner)
|
||||
}
|
||||
|
||||
// addCleanupChecked adds an operation that may return an error to be performed at the end of the test.
|
||||
// If the operation fails, the test is failed.
|
||||
func (ctx *TestContext) addCleanupChecked(f func() error, label string) {
|
||||
checkedFunction := func() {
|
||||
err := f()
|
||||
require.NoError(ctx.t, err)
|
||||
}
|
||||
|
||||
cleaner := &Cleaner{
|
||||
cleanup: checkedFunction,
|
||||
label: label,
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
if _, file, line, ok := runtime.Caller(1); ok {
|
||||
cleaner.file, cleaner.lineNumber = filepath.Base(file), line
|
||||
}
|
||||
|
||||
ctx.cleanupSteps = append(ctx.cleanupSteps, cleaner)
|
||||
}
|
||||
91
test/context/config.go
Normal file
91
test/context/config.go
Normal file
@ -0,0 +1,91 @@
|
||||
// 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 context
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
|
||||
type fakeConfig struct {
|
||||
dir string
|
||||
tm *pmapi.TokenManager
|
||||
}
|
||||
|
||||
// newFakeConfig creates a temporary folder for files.
|
||||
// It's expected the test calls `ClearData` before finish to remove it from the file system.
|
||||
func newFakeConfig() *fakeConfig {
|
||||
dir, err := ioutil.TempDir("", "example")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &fakeConfig{
|
||||
dir: dir,
|
||||
tm: pmapi.NewTokenManager(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *fakeConfig) ClearData() error {
|
||||
return os.RemoveAll(c.dir)
|
||||
}
|
||||
func (c *fakeConfig) GetAPIConfig() *pmapi.ClientConfig {
|
||||
return &pmapi.ClientConfig{
|
||||
AppVersion: "Bridge_" + os.Getenv("VERSION"),
|
||||
ClientID: "bridge",
|
||||
// TokenManager should not be required, but PMAPI still doesn't handle not-set cases everywhere.
|
||||
TokenManager: c.tm,
|
||||
}
|
||||
}
|
||||
func (c *fakeConfig) GetDBDir() string {
|
||||
return c.dir
|
||||
}
|
||||
func (c *fakeConfig) GetLogDir() string {
|
||||
return c.dir
|
||||
}
|
||||
func (c *fakeConfig) GetLogPrefix() string {
|
||||
return "test"
|
||||
}
|
||||
func (c *fakeConfig) GetPreferencesPath() string {
|
||||
return filepath.Join(c.dir, "prefs.json")
|
||||
}
|
||||
func (c *fakeConfig) GetTLSCertPath() string {
|
||||
return filepath.Join(c.dir, "cert.pem")
|
||||
}
|
||||
func (c *fakeConfig) GetTLSKeyPath() string {
|
||||
return filepath.Join(c.dir, "key.pem")
|
||||
}
|
||||
func (c *fakeConfig) GetEventsPath() string {
|
||||
return filepath.Join(c.dir, "events.json")
|
||||
}
|
||||
func (c *fakeConfig) GetIMAPCachePath() string {
|
||||
return filepath.Join(c.dir, "user_info.json")
|
||||
}
|
||||
func (c *fakeConfig) GetDefaultAPIPort() int {
|
||||
return 21042
|
||||
}
|
||||
func (c *fakeConfig) GetDefaultIMAPPort() int {
|
||||
return 21100 + rand.Intn(100)
|
||||
}
|
||||
func (c *fakeConfig) GetDefaultSMTPPort() int {
|
||||
return 21200 + rand.Intn(100)
|
||||
}
|
||||
118
test/context/context.go
Normal file
118
test/context/context.go
Normal file
@ -0,0 +1,118 @@
|
||||
// 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 context allows integration tests to be written in a fluent, english-like way.
|
||||
package context
|
||||
|
||||
import (
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/test/accounts"
|
||||
"github.com/ProtonMail/proton-bridge/test/mocks"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type server interface {
|
||||
ListenAndServe()
|
||||
Close()
|
||||
}
|
||||
|
||||
// TestContext manages a bridge test (mocked API, bridge instance, IMAP/SMTP servers, setup steps).
|
||||
type TestContext struct {
|
||||
// Base setup for the whole bridge (core & imap & smtp).
|
||||
t *bddT
|
||||
cfg *fakeConfig
|
||||
listener listener.Listener
|
||||
pmapiController PMAPIController // pmapiController is used to create pmapi clients (either real or fake) and control server state.
|
||||
testAccounts *accounts.TestAccounts
|
||||
|
||||
// Bridge core related variables.
|
||||
bridge *bridge.Bridge
|
||||
bridgeLastError error
|
||||
credStore bridge.CredentialsStorer
|
||||
|
||||
// IMAP related variables.
|
||||
imapAddr string
|
||||
imapServer server
|
||||
imapClients map[string]*mocks.IMAPClient
|
||||
imapLastResponses map[string]*mocks.IMAPResponse
|
||||
|
||||
// SMTP related variables.
|
||||
smtpAddr string
|
||||
smtpServer server
|
||||
smtpClients map[string]*mocks.SMTPClient
|
||||
smtpLastResponses map[string]*mocks.SMTPResponse
|
||||
|
||||
// These are the cleanup steps executed when Cleanup() is called.
|
||||
cleanupSteps []*Cleaner
|
||||
|
||||
// logger allows logging of test labels to be handled differently (silenced/diverted/whatever).
|
||||
logger logrus.FieldLogger
|
||||
}
|
||||
|
||||
// New returns a new test TestContext.
|
||||
func New() *TestContext {
|
||||
setLogrusVerbosityFromEnv()
|
||||
|
||||
cfg := newFakeConfig()
|
||||
|
||||
ctx := &TestContext{
|
||||
t: &bddT{},
|
||||
cfg: cfg,
|
||||
listener: listener.New(),
|
||||
pmapiController: newPMAPIController(),
|
||||
testAccounts: newTestAccounts(),
|
||||
credStore: newFakeCredStore(),
|
||||
imapClients: make(map[string]*mocks.IMAPClient),
|
||||
imapLastResponses: make(map[string]*mocks.IMAPResponse),
|
||||
smtpClients: make(map[string]*mocks.SMTPClient),
|
||||
smtpLastResponses: make(map[string]*mocks.SMTPResponse),
|
||||
logger: logrus.StandardLogger(),
|
||||
}
|
||||
|
||||
// Ensure that the config is cleaned up after the test is over.
|
||||
ctx.addCleanupChecked(cfg.ClearData, "Cleaning bridge config data")
|
||||
|
||||
// Create bridge instance under test.
|
||||
ctx.withBridgeInstance()
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
// Cleanup runs through all cleanup steps.
|
||||
// This can be a deferred call so that it is run even if the test steps failed the test.
|
||||
func (ctx *TestContext) Cleanup() *TestContext {
|
||||
for _, cleanup := range ctx.cleanupSteps {
|
||||
cleanup.Execute()
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
// GetPMAPIController returns API controller, either fake or live.
|
||||
func (ctx *TestContext) GetPMAPIController() PMAPIController {
|
||||
return ctx.pmapiController
|
||||
}
|
||||
|
||||
// GetTestingT returns testing.T compatible struct.
|
||||
func (ctx *TestContext) GetTestingT() *bddT { //nolint[golint]
|
||||
return ctx.t
|
||||
}
|
||||
|
||||
// GetTestingError returns error if test failed by using custom TestingT.
|
||||
func (ctx *TestContext) GetTestingError() error {
|
||||
return ctx.t.getErrors()
|
||||
}
|
||||
102
test/context/credentials.go
Normal file
102
test/context/credentials.go
Normal file
@ -0,0 +1,102 @@
|
||||
// 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 context
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
|
||||
)
|
||||
|
||||
// bridgePassword is password to be used for IMAP or SMTP under tests.
|
||||
const bridgePassword = "bridgepassword"
|
||||
|
||||
type fakeCredStore struct {
|
||||
credentials map[string]*credentials.Credentials
|
||||
}
|
||||
|
||||
// newFakeCredStore returns a fake credentials store (optionally configured with the given credentials).
|
||||
func newFakeCredStore(initCreds ...*credentials.Credentials) (c *fakeCredStore) {
|
||||
c = &fakeCredStore{credentials: map[string]*credentials.Credentials{}}
|
||||
for _, creds := range initCreds {
|
||||
if creds == nil {
|
||||
continue
|
||||
}
|
||||
c.credentials[creds.UserID] = &credentials.Credentials{}
|
||||
*c.credentials[creds.UserID] = *creds
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *fakeCredStore) List() (userIDs []string, err error) {
|
||||
keys := []string{}
|
||||
for key := range c.credentials {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func (c *fakeCredStore) Add(userID, userName, apiToken, mailboxPassword string, emails []string) (*credentials.Credentials, error) {
|
||||
bridgePassword := bridgePassword
|
||||
if c, ok := c.credentials[userID]; ok {
|
||||
bridgePassword = c.BridgePassword
|
||||
}
|
||||
c.credentials[userID] = &credentials.Credentials{
|
||||
UserID: userID,
|
||||
Name: userName,
|
||||
Emails: strings.Join(emails, ";"),
|
||||
APIToken: apiToken,
|
||||
MailboxPassword: mailboxPassword,
|
||||
BridgePassword: bridgePassword,
|
||||
IsCombinedAddressMode: true, // otherwise by default starts in split mode
|
||||
}
|
||||
|
||||
return c.Get(userID)
|
||||
}
|
||||
|
||||
func (c *fakeCredStore) Get(userID string) (*credentials.Credentials, error) {
|
||||
return c.credentials[userID], nil
|
||||
}
|
||||
|
||||
func (c *fakeCredStore) SwitchAddressMode(userID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *fakeCredStore) UpdateEmails(userID string, emails []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *fakeCredStore) UpdateToken(userID, apiToken string) error {
|
||||
creds, err := c.Get(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
creds.APIToken = apiToken
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *fakeCredStore) Logout(userID string) error {
|
||||
c.credentials[userID].APIToken = ""
|
||||
c.credentials[userID].MailboxPassword = ""
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *fakeCredStore) Delete(userID string) error {
|
||||
delete(c.credentials, userID)
|
||||
return nil
|
||||
}
|
||||
40
test/context/environments.go
Normal file
40
test/context/environments.go
Normal file
@ -0,0 +1,40 @@
|
||||
// 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 context
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
EnvName = "TEST_ENV"
|
||||
EnvFake = "fake"
|
||||
EnvLive = "live"
|
||||
)
|
||||
|
||||
func (ctx *TestContext) EventLoopTimeout() time.Duration {
|
||||
switch os.Getenv(EnvName) {
|
||||
case EnvFake:
|
||||
return 5 * time.Second
|
||||
case EnvLive:
|
||||
return 60 * time.Second
|
||||
default:
|
||||
panic("unknown env")
|
||||
}
|
||||
}
|
||||
80
test/context/imap.go
Normal file
80
test/context/imap.go
Normal file
@ -0,0 +1,80 @@
|
||||
// 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 context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/imap"
|
||||
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/test/mocks"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// GetIMAPClient gets the imap client by name; if it doesn't exist yet it creates it.
|
||||
func (ctx *TestContext) GetIMAPClient(handle string) *mocks.IMAPClient {
|
||||
if client, ok := ctx.imapClients[handle]; ok {
|
||||
return client
|
||||
}
|
||||
return ctx.newIMAPClient(handle)
|
||||
}
|
||||
|
||||
func (ctx *TestContext) newIMAPClient(handle string) *mocks.IMAPClient {
|
||||
ctx.withIMAPServer()
|
||||
|
||||
client := mocks.NewIMAPClient(ctx.t, handle, ctx.imapAddr)
|
||||
ctx.imapClients[handle] = client
|
||||
ctx.addCleanup(client.Close, "Closing IMAP client")
|
||||
return client
|
||||
}
|
||||
|
||||
// withIMAPServer starts an imap server and connects it to the bridge instance.
|
||||
// Every TestContext has this by default and thus this doesn't need to be exported.
|
||||
func (ctx *TestContext) withIMAPServer() {
|
||||
if ctx.imapServer != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ph := newPanicHandler(ctx.t)
|
||||
pref := preferences.New(ctx.cfg)
|
||||
port := pref.GetInt(preferences.IMAPPortKey)
|
||||
tls, _ := config.GetTLSConfig(ctx.cfg)
|
||||
|
||||
backend := imap.NewIMAPBackend(ph, ctx.listener, ctx.cfg, ctx.bridge)
|
||||
server := imap.NewIMAPServer(true, true, port, tls, backend, ctx.listener)
|
||||
|
||||
go server.ListenAndServe()
|
||||
require.NoError(ctx.t, waitForPort(port, 5*time.Second))
|
||||
|
||||
ctx.imapServer = server
|
||||
ctx.imapAddr = fmt.Sprintf("%v:%v", bridge.Host, port)
|
||||
ctx.addCleanup(ctx.imapServer.Close, "Closing IMAP server")
|
||||
}
|
||||
|
||||
// SetIMAPLastResponse sets the last IMAP response that was received.
|
||||
func (ctx *TestContext) SetIMAPLastResponse(handle string, resp *mocks.IMAPResponse) {
|
||||
ctx.imapLastResponses[handle] = resp
|
||||
}
|
||||
|
||||
// GetIMAPLastResponse returns the last IMAP response that was received.
|
||||
func (ctx *TestContext) GetIMAPLastResponse(handle string) *mocks.IMAPResponse {
|
||||
return ctx.imapLastResponses[handle]
|
||||
}
|
||||
84
test/context/pmapi_controller.go
Normal file
84
test/context/pmapi_controller.go
Normal file
@ -0,0 +1,84 @@
|
||||
// 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 context
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/ProtonMail/proton-bridge/test/fakeapi"
|
||||
"github.com/ProtonMail/proton-bridge/test/liveapi"
|
||||
)
|
||||
|
||||
type PMAPIController interface {
|
||||
GetClient(userID string) bridge.PMAPIProvider
|
||||
TurnInternetConnectionOff()
|
||||
TurnInternetConnectionOn()
|
||||
AddUser(user *pmapi.User, addresses *pmapi.AddressList, password string, twoFAEnabled bool) error
|
||||
AddUserLabel(username string, label *pmapi.Label) error
|
||||
GetLabelIDs(username string, labelNames []string) ([]string, error)
|
||||
AddUserMessage(username string, message *pmapi.Message) error
|
||||
GetMessageID(username, messageIndex string) string
|
||||
PrintCalls()
|
||||
WasCalled(method, path string, expectedRequest []byte) bool
|
||||
GetCalls(method, path string) [][]byte
|
||||
}
|
||||
|
||||
func newPMAPIController() PMAPIController {
|
||||
switch os.Getenv(EnvName) {
|
||||
case EnvFake:
|
||||
return newFakePMAPIController()
|
||||
case EnvLive:
|
||||
return newLivePMAPIController()
|
||||
default:
|
||||
panic("unknown env")
|
||||
}
|
||||
}
|
||||
|
||||
func newFakePMAPIController() PMAPIController {
|
||||
return newFakePMAPIControllerWrap(fakeapi.NewController())
|
||||
}
|
||||
|
||||
type fakePMAPIControllerWrap struct {
|
||||
*fakeapi.Controller
|
||||
}
|
||||
|
||||
func newFakePMAPIControllerWrap(controller *fakeapi.Controller) PMAPIController {
|
||||
return &fakePMAPIControllerWrap{Controller: controller}
|
||||
}
|
||||
|
||||
func (s *fakePMAPIControllerWrap) GetClient(userID string) bridge.PMAPIProvider {
|
||||
return s.Controller.GetClient(userID)
|
||||
}
|
||||
|
||||
func newLivePMAPIController() PMAPIController {
|
||||
return newLiveAPIControllerWrap(liveapi.NewController())
|
||||
}
|
||||
|
||||
type liveAPIControllerWrap struct {
|
||||
*liveapi.Controller
|
||||
}
|
||||
|
||||
func newLiveAPIControllerWrap(controller *liveapi.Controller) PMAPIController {
|
||||
return &liveAPIControllerWrap{Controller: controller}
|
||||
}
|
||||
|
||||
func (s *liveAPIControllerWrap) GetClient(userID string) bridge.PMAPIProvider {
|
||||
return s.Controller.GetClient(userID)
|
||||
}
|
||||
81
test/context/smtp.go
Normal file
81
test/context/smtp.go
Normal file
@ -0,0 +1,81 @@
|
||||
// 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 context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
||||
"github.com/ProtonMail/proton-bridge/internal/smtp"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/test/mocks"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// GetSMTPClient gets the smtp client by name; if it doesn't exist yet it creates it.
|
||||
func (ctx *TestContext) GetSMTPClient(handle string) *mocks.SMTPClient {
|
||||
if client, ok := ctx.smtpClients[handle]; ok {
|
||||
return client
|
||||
}
|
||||
return ctx.newSMTPClient(handle)
|
||||
}
|
||||
|
||||
func (ctx *TestContext) newSMTPClient(handle string) *mocks.SMTPClient {
|
||||
ctx.withSMTPServer()
|
||||
|
||||
client := mocks.NewSMTPClient(ctx.t, handle, ctx.smtpAddr)
|
||||
ctx.smtpClients[handle] = client
|
||||
ctx.addCleanup(client.Close, "Closing SMTP client")
|
||||
return client
|
||||
}
|
||||
|
||||
// withSMTPServer starts an smtp server and connects it to the bridge instance.
|
||||
// Every TestContext has this by default and thus this doesn't need to be exported.
|
||||
func (ctx *TestContext) withSMTPServer() {
|
||||
if ctx.smtpServer != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ph := newPanicHandler(ctx.t)
|
||||
pref := preferences.New(ctx.cfg)
|
||||
tls, _ := config.GetTLSConfig(ctx.cfg)
|
||||
port := pref.GetInt(preferences.SMTPPortKey)
|
||||
useSSL := pref.GetBool(preferences.SMTPSSLKey)
|
||||
|
||||
backend := smtp.NewSMTPBackend(ph, ctx.listener, pref, ctx.bridge)
|
||||
server := smtp.NewSMTPServer(true, port, useSSL, tls, backend, ctx.listener)
|
||||
|
||||
go server.ListenAndServe()
|
||||
require.NoError(ctx.t, waitForPort(port, 5*time.Second))
|
||||
|
||||
ctx.smtpServer = server
|
||||
ctx.smtpAddr = fmt.Sprintf("%v:%v", bridge.Host, port)
|
||||
ctx.addCleanup(ctx.smtpServer.Close, "Closing SMTP server")
|
||||
}
|
||||
|
||||
// SetSMTPLastResponse sets the last SMTP response that was received.
|
||||
func (ctx *TestContext) SetSMTPLastResponse(handle string, resp *mocks.SMTPResponse) {
|
||||
ctx.smtpLastResponses[handle] = resp
|
||||
}
|
||||
|
||||
// GetSMTPLastResponse returns the last IMAP response that was received.
|
||||
func (ctx *TestContext) GetSMTPLastResponse(handle string) *mocks.SMTPResponse {
|
||||
return ctx.smtpLastResponses[handle]
|
||||
}
|
||||
87
test/context/utils.go
Normal file
87
test/context/utils.go
Normal file
@ -0,0 +1,87 @@
|
||||
// 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 context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func waitForPort(port int, timeout time.Duration) error {
|
||||
return waitUntilTrue(timeout, func() bool {
|
||||
conn, err := net.DialTimeout("tcp", "127.0.0.1:"+strconv.Itoa(port), timeout)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if conn != nil {
|
||||
if err := conn.Close(); err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// waitUntilTrue can use Eventually from
|
||||
// https://godoc.org/github.com/stretchr/testify/require#Assertions.Eventually
|
||||
func waitUntilTrue(timeout time.Duration, callback func() bool) error {
|
||||
endTime := time.Now().Add(timeout)
|
||||
for {
|
||||
if time.Now().After(endTime) {
|
||||
return fmt.Errorf("Timeout")
|
||||
}
|
||||
if callback() {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func setLogrusVerbosityFromEnv() {
|
||||
verbosityName := os.Getenv("VERBOSITY")
|
||||
switch strings.ToLower(verbosityName) {
|
||||
case "panic":
|
||||
logrus.SetLevel(logrus.PanicLevel)
|
||||
case "fatal":
|
||||
logrus.SetLevel(logrus.FatalLevel)
|
||||
case "error":
|
||||
logrus.SetLevel(logrus.ErrorLevel)
|
||||
case "warning", "warn":
|
||||
logrus.SetLevel(logrus.WarnLevel)
|
||||
case "info":
|
||||
logrus.SetLevel(logrus.InfoLevel)
|
||||
case "debug":
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
case "trace":
|
||||
logrus.SetLevel(logrus.TraceLevel)
|
||||
default:
|
||||
logrus.SetLevel(logrus.FatalLevel)
|
||||
}
|
||||
|
||||
logrus.SetFormatter(&logrus.TextFormatter{
|
||||
ForceColors: true,
|
||||
FullTimestamp: true,
|
||||
TimestampFormat: time.StampMilli,
|
||||
})
|
||||
}
|
||||
47
test/fakeapi/attachments.go
Normal file
47
test/fakeapi/attachments.go
Normal file
@ -0,0 +1,47 @@
|
||||
// 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 fakeapi
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
|
||||
func (api *FakePMAPI) GetAttachment(attachmentID string) (io.ReadCloser, error) {
|
||||
if err := api.checkAndRecordCall(GET, "/attachments/"+attachmentID, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := strings.NewReader("data")
|
||||
return ioutil.NopCloser(data), nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) CreateAttachment(attachment *pmapi.Attachment, data io.Reader, signature io.Reader) (*pmapi.Attachment, error) {
|
||||
if err := api.checkAndRecordCall(POST, "/attachments", nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes, err := ioutil.ReadAll(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
attachment.KeyPackets = base64.StdEncoding.EncodeToString(bytes)
|
||||
return attachment, nil
|
||||
}
|
||||
153
test/fakeapi/auth.go
Normal file
153
test/fakeapi/auth.go
Normal file
@ -0,0 +1,153 @@
|
||||
// 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 fakeapi
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
|
||||
func (api *FakePMAPI) SetAuths(auths chan<- *pmapi.Auth) {
|
||||
api.auths = auths
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) AuthInfo(username string) (*pmapi.AuthInfo, error) {
|
||||
if err := api.checkInternetAndRecordCall(POST, "/auth/info", &pmapi.AuthInfoReq{
|
||||
Username: username,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authInfo := &pmapi.AuthInfo{}
|
||||
user, ok := api.controller.usersByUsername[username]
|
||||
if !ok {
|
||||
// If username is wrong, API server will return empty but
|
||||
// positive response
|
||||
return authInfo, nil
|
||||
}
|
||||
authInfo.TwoFA = user.get2FAInfo()
|
||||
return authInfo, nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) Auth(username, password string, authInfo *pmapi.AuthInfo) (*pmapi.Auth, error) {
|
||||
if err := api.checkInternetAndRecordCall(POST, "/auth", &pmapi.AuthReq{
|
||||
Username: username,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session, err := api.controller.createSessionIfAuthorized(username, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
api.setUID(session.uid)
|
||||
|
||||
if err := api.setUser(username); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := api.controller.usersByUsername[username]
|
||||
auth := &pmapi.Auth{
|
||||
TwoFA: user.get2FAInfo(),
|
||||
RefreshToken: session.refreshToken,
|
||||
}
|
||||
|
||||
api.sendAuth(auth)
|
||||
return auth, nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) Auth2FA(twoFactorCode string, auth *pmapi.Auth) (*pmapi.Auth2FA, error) {
|
||||
if err := api.checkInternetAndRecordCall(POST, "/auth/2fa", &pmapi.Auth2FAReq{
|
||||
TwoFactorCode: twoFactorCode,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if api.uid == "" {
|
||||
return nil, pmapi.ErrInvalidToken
|
||||
}
|
||||
|
||||
session, ok := api.controller.sessionsByUID[api.uid]
|
||||
if !ok {
|
||||
return nil, pmapi.ErrInvalidToken
|
||||
}
|
||||
|
||||
session.hasFullScope = true
|
||||
|
||||
return &pmapi.Auth2FA{
|
||||
Scope: "full",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) AuthRefresh(token string) (*pmapi.Auth, error) {
|
||||
if api.lastToken == "" {
|
||||
api.lastToken = token
|
||||
}
|
||||
|
||||
split := strings.Split(token, ":")
|
||||
if len(split) != 2 {
|
||||
return nil, pmapi.ErrInvalidToken
|
||||
}
|
||||
|
||||
if err := api.checkInternetAndRecordCall(POST, "/auth/refresh", &pmapi.AuthRefreshReq{
|
||||
ResponseType: "token",
|
||||
GrantType: "refresh_token",
|
||||
UID: split[0],
|
||||
RefreshToken: split[1],
|
||||
RedirectURI: "https://protonmail.ch",
|
||||
State: "random_string",
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session, ok := api.controller.sessionsByUID[split[0]]
|
||||
if !ok || session.refreshToken != split[1] {
|
||||
api.log.WithField("token", token).
|
||||
WithField("session", session).
|
||||
Warn("Refresh token failed")
|
||||
// The API server will respond normal error not 401 (check api)
|
||||
// i.e. should not use `sendAuth(nil)`
|
||||
api.setUID("")
|
||||
return nil, pmapi.ErrInvalidToken
|
||||
}
|
||||
api.setUID(split[0])
|
||||
|
||||
if err := api.setUser(session.username); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
api.controller.refreshTheTokensForSession(session)
|
||||
api.lastToken = split[0] + ":" + session.refreshToken
|
||||
|
||||
auth := &pmapi.Auth{
|
||||
RefreshToken: session.refreshToken,
|
||||
}
|
||||
api.sendAuth(auth)
|
||||
|
||||
return auth, nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) Logout() error {
|
||||
if err := api.checkAndRecordCall(DELETE, "/auth", nil); err != nil {
|
||||
return err
|
||||
}
|
||||
// Logout will also emit change to auth channel
|
||||
api.sendAuth(nil)
|
||||
api.controller.deleteSession(api.uid)
|
||||
api.unsetUser()
|
||||
return nil
|
||||
}
|
||||
50
test/fakeapi/contacts.go
Normal file
50
test/fakeapi/contacts.go
Normal file
@ -0,0 +1,50 @@
|
||||
// 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 fakeapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
|
||||
func (api *FakePMAPI) DecryptAndVerifyCards(cards []pmapi.Card) ([]pmapi.Card, error) {
|
||||
return cards, nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) GetContactEmailByEmail(email string, page int, pageSize int) ([]pmapi.ContactEmail, error) {
|
||||
v := url.Values{}
|
||||
v.Set("Page", strconv.Itoa(page))
|
||||
if pageSize > 0 {
|
||||
v.Set("PageSize", strconv.Itoa(pageSize))
|
||||
}
|
||||
v.Set("Email", email)
|
||||
if err := api.checkAndRecordCall(GET, "/contacts/emails?"+v.Encode(), nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []pmapi.ContactEmail{}, nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) GetContactByID(contactID string) (pmapi.Contact, error) {
|
||||
if err := api.checkAndRecordCall(GET, "/contacts/"+contactID, nil); err != nil {
|
||||
return pmapi.Contact{}, err
|
||||
}
|
||||
return pmapi.Contact{}, fmt.Errorf("contact %s does not exist", contactID)
|
||||
}
|
||||
71
test/fakeapi/controller.go
Normal file
71
test/fakeapi/controller.go
Normal file
@ -0,0 +1,71 @@
|
||||
// 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 fakeapi
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
// Internal states.
|
||||
lock *sync.RWMutex
|
||||
fakeAPIs []*FakePMAPI
|
||||
calls []*fakeCall
|
||||
labelIDGenerator idGenerator
|
||||
messageIDGenerator idGenerator
|
||||
tokenGenerator idGenerator
|
||||
|
||||
// State controlled by test.
|
||||
noInternetConnection bool
|
||||
usersByUsername map[string]*fakeUser
|
||||
sessionsByUID map[string]*fakeSession
|
||||
addressesByUsername map[string]*pmapi.AddressList
|
||||
labelsByUsername map[string][]*pmapi.Label
|
||||
messagesByUsername map[string][]*pmapi.Message
|
||||
|
||||
log *logrus.Entry
|
||||
}
|
||||
|
||||
func NewController() *Controller {
|
||||
return &Controller{
|
||||
lock: &sync.RWMutex{},
|
||||
fakeAPIs: []*FakePMAPI{},
|
||||
calls: []*fakeCall{},
|
||||
labelIDGenerator: 100, // We cannot use system label IDs.
|
||||
messageIDGenerator: 0,
|
||||
tokenGenerator: 1000, // No specific reason; 1000 simply feels right.
|
||||
|
||||
noInternetConnection: false,
|
||||
usersByUsername: map[string]*fakeUser{},
|
||||
sessionsByUID: map[string]*fakeSession{},
|
||||
addressesByUsername: map[string]*pmapi.AddressList{},
|
||||
labelsByUsername: map[string][]*pmapi.Label{},
|
||||
messagesByUsername: map[string][]*pmapi.Message{},
|
||||
|
||||
log: logrus.WithField("pkg", "fakeapi-controller"),
|
||||
}
|
||||
}
|
||||
|
||||
func (cntrl *Controller) GetClient(userID string) *FakePMAPI {
|
||||
fakeAPI := New(cntrl)
|
||||
cntrl.fakeAPIs = append(cntrl.fakeAPIs, fakeAPI)
|
||||
return fakeAPI
|
||||
}
|
||||
93
test/fakeapi/controller_calls.go
Normal file
93
test/fakeapi/controller_calls.go
Normal file
@ -0,0 +1,93 @@
|
||||
// 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 fakeapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/nsf/jsondiff"
|
||||
)
|
||||
|
||||
type method string
|
||||
|
||||
const (
|
||||
GET method = "GET"
|
||||
POST method = "POST"
|
||||
PUT method = "PUT"
|
||||
DELETE method = "DELETE"
|
||||
)
|
||||
|
||||
type fakeCall struct {
|
||||
method method
|
||||
path string
|
||||
request []byte
|
||||
}
|
||||
|
||||
func (cntrl *Controller) recordCall(method method, path string, req interface{}) {
|
||||
cntrl.lock.Lock()
|
||||
defer cntrl.lock.Unlock()
|
||||
|
||||
request := []byte{}
|
||||
if req != nil {
|
||||
var err error
|
||||
request, err = json.Marshal(req)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
cntrl.calls = append(cntrl.calls, &fakeCall{
|
||||
method: method,
|
||||
path: path,
|
||||
request: request,
|
||||
})
|
||||
}
|
||||
|
||||
func (cntrl *Controller) PrintCalls() {
|
||||
fmt.Println("API calls:")
|
||||
for idx, call := range cntrl.calls {
|
||||
fmt.Printf("%02d: [%s] %s\n", idx+1, call.method, call.path)
|
||||
if call.request != nil && string(call.request) != "null" {
|
||||
fmt.Printf("\t%s\n", call.request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cntrl *Controller) WasCalled(method, path string, expectedRequest []byte) bool {
|
||||
for _, call := range cntrl.calls {
|
||||
if string(call.method) != method && call.path != path {
|
||||
continue
|
||||
}
|
||||
diff, _ := jsondiff.Compare(call.request, expectedRequest, &jsondiff.Options{})
|
||||
isSuperset := diff == jsondiff.FullMatch || diff == jsondiff.SupersetMatch
|
||||
if isSuperset {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (cntrl *Controller) GetCalls(method, path string) [][]byte {
|
||||
requests := [][]byte{}
|
||||
for _, call := range cntrl.calls {
|
||||
if string(call.method) == method && call.path == path {
|
||||
requests = append(requests, call.request)
|
||||
}
|
||||
}
|
||||
return requests
|
||||
}
|
||||
142
test/fakeapi/controller_control.go
Normal file
142
test/fakeapi/controller_control.go
Normal file
@ -0,0 +1,142 @@
|
||||
// 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 fakeapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
|
||||
var systemLabelNameToID = map[string]string{ //nolint[gochecknoglobals]
|
||||
"INBOX": pmapi.InboxLabel,
|
||||
"Trash": pmapi.TrashLabel,
|
||||
"Spam": pmapi.SpamLabel,
|
||||
"All Mail": pmapi.AllMailLabel,
|
||||
"Archive": pmapi.ArchiveLabel,
|
||||
"Sent": pmapi.SentLabel,
|
||||
"Drafts": pmapi.DraftLabel,
|
||||
}
|
||||
|
||||
func (cntrl *Controller) TurnInternetConnectionOff() {
|
||||
cntrl.log.Warn("Turning OFF internet")
|
||||
cntrl.noInternetConnection = true
|
||||
}
|
||||
|
||||
func (cntrl *Controller) TurnInternetConnectionOn() {
|
||||
cntrl.log.Warn("Turning ON internet")
|
||||
cntrl.noInternetConnection = false
|
||||
}
|
||||
|
||||
func (cntrl *Controller) AddUser(user *pmapi.User, addresses *pmapi.AddressList, password string, twoFAEnabled bool) error {
|
||||
cntrl.usersByUsername[user.Name] = &fakeUser{
|
||||
user: user,
|
||||
password: password,
|
||||
has2FA: twoFAEnabled,
|
||||
}
|
||||
cntrl.addressesByUsername[user.Name] = addresses
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cntrl *Controller) AddUserLabel(username string, label *pmapi.Label) error {
|
||||
if _, ok := cntrl.labelsByUsername[username]; !ok {
|
||||
cntrl.labelsByUsername[username] = []*pmapi.Label{}
|
||||
}
|
||||
|
||||
labelName := getLabelNameWithoutPrefix(label.Name)
|
||||
for _, existingLabel := range cntrl.labelsByUsername[username] {
|
||||
if existingLabel.Name == labelName {
|
||||
return fmt.Errorf("folder or label %s already exists", label.Name)
|
||||
}
|
||||
}
|
||||
|
||||
label.Exclusive = getLabelExclusive(label.Name)
|
||||
prefix := "label"
|
||||
if label.Exclusive == 1 {
|
||||
prefix = "folder"
|
||||
}
|
||||
label.ID = cntrl.labelIDGenerator.next(prefix)
|
||||
label.Name = labelName
|
||||
cntrl.labelsByUsername[username] = append(cntrl.labelsByUsername[username], label)
|
||||
cntrl.resetUsers()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cntrl *Controller) GetLabelIDs(username string, labelNames []string) ([]string, error) {
|
||||
labelIDs := []string{}
|
||||
for _, labelName := range labelNames {
|
||||
labelID, err := cntrl.getLabelID(username, labelName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
labelIDs = append(labelIDs, labelID)
|
||||
}
|
||||
return labelIDs, nil
|
||||
}
|
||||
|
||||
func (cntrl *Controller) getLabelID(username, labelName string) (string, error) {
|
||||
if labelID, ok := systemLabelNameToID[labelName]; ok {
|
||||
return labelID, nil
|
||||
}
|
||||
labelName = getLabelNameWithoutPrefix(labelName)
|
||||
for _, label := range cntrl.labelsByUsername[username] {
|
||||
if label.Name == labelName {
|
||||
return label.ID, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("label %s:%s does not exist", username, labelName)
|
||||
}
|
||||
|
||||
func getLabelNameWithoutPrefix(name string) string {
|
||||
if strings.HasPrefix(name, "Folders/") {
|
||||
return strings.TrimPrefix(name, "Folders/")
|
||||
}
|
||||
if strings.HasPrefix(name, "Labels/") {
|
||||
return strings.TrimPrefix(name, "Labels/")
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func getLabelExclusive(name string) int {
|
||||
if strings.HasPrefix(name, "Folders/") {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (cntrl *Controller) AddUserMessage(username string, message *pmapi.Message) error {
|
||||
if _, ok := cntrl.messagesByUsername[username]; !ok {
|
||||
cntrl.messagesByUsername[username] = []*pmapi.Message{}
|
||||
}
|
||||
message.ID = cntrl.messageIDGenerator.next("")
|
||||
message.LabelIDs = append(message.LabelIDs, pmapi.AllMailLabel)
|
||||
cntrl.messagesByUsername[username] = append(cntrl.messagesByUsername[username], message)
|
||||
cntrl.resetUsers()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cntrl *Controller) resetUsers() {
|
||||
for _, fakeAPI := range cntrl.fakeAPIs {
|
||||
_ = fakeAPI.setUser(fakeAPI.username)
|
||||
}
|
||||
}
|
||||
|
||||
func (cntrl *Controller) GetMessageID(username, messageIndex string) string {
|
||||
return messageIndex
|
||||
}
|
||||
57
test/fakeapi/controller_session.go
Normal file
57
test/fakeapi/controller_session.go
Normal file
@ -0,0 +1,57 @@
|
||||
// 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 fakeapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
type fakeSession struct {
|
||||
username string
|
||||
uid, refreshToken string
|
||||
hasFullScope bool
|
||||
}
|
||||
|
||||
var errWrongNameOrPassword = errors.New("Incorrect login credentials. Please try again") //nolint[stylecheck]
|
||||
|
||||
func (cntrl *Controller) createSessionIfAuthorized(username, password string) (*fakeSession, error) {
|
||||
// get user
|
||||
user, ok := cntrl.usersByUsername[username]
|
||||
if !ok || user.password != password {
|
||||
return nil, errWrongNameOrPassword
|
||||
}
|
||||
|
||||
// create session
|
||||
session := &fakeSession{
|
||||
username: username,
|
||||
uid: cntrl.tokenGenerator.next("uid"),
|
||||
hasFullScope: !user.has2FA,
|
||||
}
|
||||
cntrl.refreshTheTokensForSession(session)
|
||||
|
||||
cntrl.sessionsByUID[session.uid] = session
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (cntrl *Controller) refreshTheTokensForSession(session *fakeSession) {
|
||||
session.refreshToken = cntrl.tokenGenerator.next("refresh")
|
||||
}
|
||||
|
||||
func (cntrl *Controller) deleteSession(uid string) {
|
||||
delete(cntrl.sessionsByUID, uid)
|
||||
}
|
||||
37
test/fakeapi/controller_user.go
Normal file
37
test/fakeapi/controller_user.go
Normal file
@ -0,0 +1,37 @@
|
||||
// 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 fakeapi
|
||||
|
||||
import "github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
|
||||
type fakeUser struct {
|
||||
user *pmapi.User
|
||||
password string
|
||||
has2FA bool
|
||||
}
|
||||
|
||||
func (fu *fakeUser) get2FAInfo() *pmapi.TwoFactorInfo {
|
||||
twoFAEnabled := 0
|
||||
if fu.has2FA {
|
||||
twoFAEnabled = 1
|
||||
}
|
||||
return &pmapi.TwoFactorInfo{
|
||||
Enabled: twoFAEnabled,
|
||||
TOTP: 0,
|
||||
}
|
||||
}
|
||||
60
test/fakeapi/counts.go
Normal file
60
test/fakeapi/counts.go
Normal file
@ -0,0 +1,60 @@
|
||||
// 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 fakeapi
|
||||
|
||||
import "github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
|
||||
func (api *FakePMAPI) CountMessages(addressID string) ([]*pmapi.MessagesCount, error) {
|
||||
if err := api.checkAndRecordCall(GET, "/messages/count?AddressID="+addressID, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return api.getCounts(addressID), nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) getAllCounts() []*pmapi.MessagesCount {
|
||||
return api.getCounts("")
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) getCounts(addressID string) []*pmapi.MessagesCount {
|
||||
allCounts := map[string]*pmapi.MessagesCount{}
|
||||
for _, message := range api.messages {
|
||||
if addressID != "" && message.AddressID != addressID {
|
||||
continue
|
||||
}
|
||||
for _, labelID := range message.LabelIDs {
|
||||
if counts, ok := allCounts[labelID]; ok {
|
||||
counts.Total++
|
||||
if message.Unread == 1 {
|
||||
counts.Unread++
|
||||
}
|
||||
} else {
|
||||
allCounts[labelID] = &pmapi.MessagesCount{
|
||||
LabelID: labelID,
|
||||
Total: 1,
|
||||
Unread: message.Unread,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res := []*pmapi.MessagesCount{}
|
||||
for _, counts := range allCounts {
|
||||
res = append(res, counts)
|
||||
}
|
||||
return res
|
||||
}
|
||||
105
test/fakeapi/events.go
Normal file
105
test/fakeapi/events.go
Normal file
@ -0,0 +1,105 @@
|
||||
// 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 fakeapi
|
||||
|
||||
import (
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
|
||||
func (api *FakePMAPI) GetEvent(eventID string) (*pmapi.Event, error) {
|
||||
if err := api.checkAndRecordCall(GET, "/events/"+eventID, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Request for empty ID returns the latest event.
|
||||
if eventID == "" {
|
||||
return api.events[len(api.events)-1], nil
|
||||
}
|
||||
// Otherwise it tries to find specific ID and return all next events merged into one.
|
||||
var foundEvent *pmapi.Event
|
||||
mergedEvent := &pmapi.Event{}
|
||||
for _, event := range api.events {
|
||||
if event.EventID == eventID {
|
||||
foundEvent = event
|
||||
continue
|
||||
}
|
||||
if foundEvent != nil {
|
||||
mergedEvent.EventID = event.EventID
|
||||
mergedEvent.Refresh |= event.Refresh
|
||||
mergedEvent.Messages = append(mergedEvent.Messages, event.Messages...)
|
||||
mergedEvent.MessageCounts = append(mergedEvent.MessageCounts, event.MessageCounts...)
|
||||
mergedEvent.Labels = append(mergedEvent.Labels, event.Labels...)
|
||||
mergedEvent.Notices = append(mergedEvent.Notices, event.Notices...)
|
||||
mergedEvent.User = event.User
|
||||
}
|
||||
}
|
||||
// If there isn't next event, return the same one.
|
||||
if mergedEvent.EventID == "" {
|
||||
return foundEvent, nil
|
||||
}
|
||||
return mergedEvent, nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) addEventLabel(action pmapi.EventAction, label *pmapi.Label) {
|
||||
api.addEvent(&pmapi.Event{
|
||||
EventID: api.eventIDGenerator.next("event"),
|
||||
Labels: []*pmapi.EventLabel{{
|
||||
EventItem: pmapi.EventItem{
|
||||
ID: label.ID,
|
||||
Action: action,
|
||||
},
|
||||
Label: label,
|
||||
}},
|
||||
})
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) addEventMessage(action pmapi.EventAction, message *pmapi.Message) {
|
||||
created := message
|
||||
updated := &pmapi.EventMessageUpdated{
|
||||
ID: message.ID,
|
||||
Subject: &message.Subject,
|
||||
Unread: &message.Unread,
|
||||
Flags: &message.Flags,
|
||||
Sender: message.Sender,
|
||||
ToList: &message.ToList,
|
||||
CCList: &message.CCList,
|
||||
BCCList: &message.BCCList,
|
||||
Time: message.Time,
|
||||
LabelIDs: message.LabelIDs,
|
||||
}
|
||||
if action == pmapi.EventCreate {
|
||||
updated = nil
|
||||
} else {
|
||||
created = nil
|
||||
}
|
||||
api.addEvent(&pmapi.Event{
|
||||
EventID: api.eventIDGenerator.next("event"),
|
||||
Messages: []*pmapi.EventMessage{{
|
||||
EventItem: pmapi.EventItem{
|
||||
ID: message.ID,
|
||||
Action: action,
|
||||
},
|
||||
Created: created,
|
||||
Updated: updated,
|
||||
}},
|
||||
MessageCounts: api.getAllCounts(),
|
||||
})
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) addEvent(event *pmapi.Event) {
|
||||
api.events = append(api.events, event)
|
||||
}
|
||||
158
test/fakeapi/fakeapi.go
Normal file
158
test/fakeapi/fakeapi.go
Normal file
@ -0,0 +1,158 @@
|
||||
// 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 fakeapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var errBadRequest = errors.New("NOT OK: 400 Bad Request")
|
||||
|
||||
type FakePMAPI struct {
|
||||
username string
|
||||
controller *Controller
|
||||
eventIDGenerator idGenerator
|
||||
|
||||
auths chan<- *pmapi.Auth
|
||||
user *pmapi.User
|
||||
addresses *pmapi.AddressList
|
||||
labels []*pmapi.Label
|
||||
messages []*pmapi.Message
|
||||
events []*pmapi.Event
|
||||
|
||||
// uid represents the API UID. It is the unique session ID.
|
||||
uid, lastToken string
|
||||
|
||||
log *logrus.Entry
|
||||
}
|
||||
|
||||
func New(controller *Controller) *FakePMAPI {
|
||||
fakePMAPI := &FakePMAPI{
|
||||
controller: controller,
|
||||
log: logrus.WithField("pkg", "fakeapi"),
|
||||
}
|
||||
fakePMAPI.addEvent(&pmapi.Event{
|
||||
EventID: fakePMAPI.eventIDGenerator.last("event"),
|
||||
Refresh: 0,
|
||||
More: 0,
|
||||
})
|
||||
return fakePMAPI
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) checkAndRecordCall(method method, path string, request interface{}) error {
|
||||
if err := api.checkInternetAndRecordCall(method, path, request); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Try re-auth
|
||||
if api.uid == "" && api.lastToken != "" {
|
||||
api.log.WithField("lastToken", api.lastToken).Warn("Handling unauthorized status")
|
||||
if _, err := api.AuthRefresh(api.lastToken); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Check client is authenticated. There is difference between
|
||||
// * invalid token
|
||||
// * and missing token
|
||||
// but API treats it the same
|
||||
if api.uid == "" {
|
||||
return pmapi.ErrInvalidToken
|
||||
}
|
||||
|
||||
// Any route (except Auth and AuthRefresh) can end with wrong
|
||||
// token and it should be translated into logout
|
||||
session, ok := api.controller.sessionsByUID[api.uid]
|
||||
if !ok {
|
||||
api.setUID("") // all consecutive requests will not send auth nil
|
||||
api.sendAuth(nil)
|
||||
return pmapi.ErrInvalidToken
|
||||
} else if !session.hasFullScope {
|
||||
// This is exact error string from the server (at least from documentation).
|
||||
return errors.New("Access token does not have sufficient scope") //nolint[stylecheck]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) checkInternetAndRecordCall(method method, path string, request interface{}) error {
|
||||
api.log.WithField(string(method), path).Trace("CALL")
|
||||
api.controller.recordCall(method, path, request)
|
||||
if api.controller.noInternetConnection {
|
||||
return pmapi.ErrAPINotReachable
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) sendAuth(auth *pmapi.Auth) {
|
||||
if auth != nil {
|
||||
auth.DANGEROUSLYSetUID(api.uid)
|
||||
}
|
||||
if api.auths != nil {
|
||||
api.auths <- auth
|
||||
}
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) setUser(username string) error {
|
||||
api.username = username
|
||||
api.log = api.log.WithField("username", username)
|
||||
|
||||
user, ok := api.controller.usersByUsername[username]
|
||||
if !ok {
|
||||
return fmt.Errorf("user %s does not exist", username)
|
||||
}
|
||||
api.user = user.user
|
||||
|
||||
addresses, ok := api.controller.addressesByUsername[username]
|
||||
if !ok {
|
||||
addresses = &pmapi.AddressList{}
|
||||
}
|
||||
api.addresses = addresses
|
||||
|
||||
labels, ok := api.controller.labelsByUsername[username]
|
||||
if !ok {
|
||||
labels = []*pmapi.Label{}
|
||||
}
|
||||
api.labels = labels
|
||||
|
||||
messages, ok := api.controller.messagesByUsername[username]
|
||||
if !ok {
|
||||
messages = []*pmapi.Message{}
|
||||
}
|
||||
api.messages = messages
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) setUID(uid string) {
|
||||
api.uid = uid
|
||||
api.log = api.log.WithField("uid", api.uid)
|
||||
api.log.Info("UID updated")
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) unsetUser() {
|
||||
api.setUID("")
|
||||
api.user = nil
|
||||
api.labels = nil
|
||||
api.messages = nil
|
||||
api.events = nil
|
||||
}
|
||||
31
test/fakeapi/idgenerator.go
Normal file
31
test/fakeapi/idgenerator.go
Normal file
@ -0,0 +1,31 @@
|
||||
// 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 fakeapi
|
||||
|
||||
import "fmt"
|
||||
|
||||
type idGenerator int
|
||||
|
||||
func (g *idGenerator) last(prefix string) string {
|
||||
return fmt.Sprintf("%s%d", prefix, *g)
|
||||
}
|
||||
|
||||
func (g *idGenerator) next(prefix string) string {
|
||||
(*g)++
|
||||
return fmt.Sprintf("%s%d", prefix, *g)
|
||||
}
|
||||
65
test/fakeapi/keys.go
Normal file
65
test/fakeapi/keys.go
Normal file
@ -0,0 +1,65 @@
|
||||
// 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 fakeapi
|
||||
|
||||
import "github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
|
||||
// publicKey is used from pmapi unit tests.
|
||||
// For now we need just some key, no need to have some specific one.
|
||||
const publicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Version: OpenPGP.js v0.7.1
|
||||
Comment: http://openpgpjs.org
|
||||
|
||||
xsBNBFRJbc0BCAC0mMLZPDBbtSCWvxwmOfXfJkE2+ssM3ux21LhD/bPiWefE
|
||||
WSHlCjJ8PqPHy7snSiUuxuj3f9AvXPvg+mjGLBwu1/QsnSP24sl3qD2onl39
|
||||
vPiLJXUqZs20ZRgnvX70gjkgEzMFBxINiy2MTIG+4RU8QA7y8KzWev0btqKi
|
||||
MeVa+GLEHhgZ2KPOn4Jv1q4bI9hV0C9NUe2tTXS6/Vv3vbCY7lRR0kbJ65T5
|
||||
c8CmpqJuASIJNrSXM/Q3NnnsY4kBYH0s5d2FgbASQvzrjuC2rngUg0EoPsrb
|
||||
DEVRA2/BCJonw7aASiNCrSP92lkZdtYlax/pcoE/mQ4WSwySFmcFT7yFABEB
|
||||
AAHNBlVzZXJJRMLAcgQQAQgAJgUCVEltzwYLCQgHAwIJED62JZ7fId8kBBUI
|
||||
AgoDFgIBAhsDAh4BAAD0nQf9EtH9TC0JqSs8q194Zo244jjlJFM3EzxOSULq
|
||||
0zbywlLORfyoo/O8jU/HIuGz+LT98JDtnltTqfjWgu6pS3ZL2/L4AGUKEoB7
|
||||
OI6oIdRwzMc61sqI+Qpbzxo7rzufH4CiXZc6cxORUgL550xSCcqnq0q1mds7
|
||||
h5roKDzxMW6WLiEsc1dN8IQKzC7Ec5wA7U4oNGsJ3TyI8jkIs0IhXrRCd26K
|
||||
0TW8Xp6GCsfblWXosR13y89WVNgC+xrrJKTZEisc0tRlneIgjcwEUvwfIg2n
|
||||
9cDUFA/5BsfzTW5IurxqDEziIVP0L44PXjtJrBQaGMPlEbtP5i2oi3OADVX2
|
||||
XbvsRc7ATQRUSW3PAQgAkPnu5fps5zhOB/e618v/iF3KiogxUeRhA68TbvA+
|
||||
xnFfTxCx2Vo14aOL0CnaJ8gO5yRSqfomL2O1kMq07N1MGbqucbmc+aSfoElc
|
||||
+Gd5xBE/w3RcEhKcAaYTi35vG22zlZup4x3ElioyIarOssFEkQgNNyDf5AXZ
|
||||
jdHLA6qVxeqAb/Ff74+y9HUmLPSsRU9NwFzvK3Jv8C/ubHVLzTYdFgYkc4W1
|
||||
Uug9Ou08K+/4NEMrwnPFBbZdJAuUjQz2zW2ZiEKiBggiorH2o5N3mYUnWEmU
|
||||
vqL3EOS8TbWo8UBIW3DDm2JiZR8VrEgvBtc9mVDUj/x+5pR07Fy1D6DjRmAc
|
||||
9wARAQABwsBfBBgBCAATBQJUSW3SCRA+tiWe3yHfJAIbDAAA/iwH/ik9RKZM
|
||||
B9Ir0x5mGpKPuqhugwrc3d04m1sOdXJm2NtD4ddzSEvzHwaPNvEvUl5v7FVM
|
||||
zf6+6mYGWHyNP4+e7RtwYLlRpud6smuGyDSsotUYyumiqP6680ZIeWVQ+a1T
|
||||
ThNs878mAJy1FhvQFdTmA8XIC616hDFpamQKPlpoO1a0wZnQhrPwT77HDYEE
|
||||
a+hqY4Jr/a7ui40S+7xYRHKL/7ZAS4/grWllhU3dbNrwSzrOKwrA/U0/9t73
|
||||
8Ap6JL71YymDeaL4sutcoaahda1pTrMWePtrCltz6uySwbZs7GXoEzjX3EAH
|
||||
+6qhkUJtzMaE3YEFEoQMGzcDTUEfXCJ3zJw=
|
||||
=yT9U
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
`
|
||||
|
||||
func (api *FakePMAPI) GetPublicKeysForEmail(email string) (keys []pmapi.PublicKey, internal bool, err error) {
|
||||
if err := api.checkAndRecordCall(GET, "/keys?Email="+email, nil); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return []pmapi.PublicKey{{
|
||||
PublicKey: publicKey,
|
||||
}}, true, nil
|
||||
}
|
||||
90
test/fakeapi/labels.go
Normal file
90
test/fakeapi/labels.go
Normal file
@ -0,0 +1,90 @@
|
||||
// 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 fakeapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
|
||||
func (api *FakePMAPI) isLabelFolder(labelID string) bool {
|
||||
for _, label := range api.labels {
|
||||
if label.ID == labelID {
|
||||
return label.Exclusive == 1
|
||||
}
|
||||
}
|
||||
return labelID == pmapi.InboxLabel || labelID == pmapi.ArchiveLabel || labelID == pmapi.SentLabel
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) ListLabels() ([]*pmapi.Label, error) {
|
||||
if err := api.checkAndRecordCall(GET, "/labels/1", nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return api.labels, nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) CreateLabel(label *pmapi.Label) (*pmapi.Label, error) {
|
||||
if err := api.checkAndRecordCall(POST, "/labels", &pmapi.LabelReq{Label: label}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, existingLabel := range api.labels {
|
||||
if existingLabel.Name == label.Name {
|
||||
return nil, fmt.Errorf("folder or label %s already exists", label.Name)
|
||||
}
|
||||
}
|
||||
prefix := "label"
|
||||
if label.Exclusive == 1 {
|
||||
prefix = "folder"
|
||||
}
|
||||
label.ID = api.controller.labelIDGenerator.next(prefix)
|
||||
api.labels = append(api.labels, label)
|
||||
api.addEventLabel(pmapi.EventCreate, label)
|
||||
return label, nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) UpdateLabel(label *pmapi.Label) (*pmapi.Label, error) {
|
||||
if err := api.checkAndRecordCall(PUT, "/labels", &pmapi.LabelReq{Label: label}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for idx, existingLabel := range api.labels {
|
||||
if existingLabel.ID == label.ID {
|
||||
// Request doesn't have to include all properties and these have to stay the same.
|
||||
label.Type = existingLabel.Type
|
||||
label.Exclusive = existingLabel.Exclusive
|
||||
api.labels[idx] = label
|
||||
api.addEventLabel(pmapi.EventUpdate, label)
|
||||
return label, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("label %s does not exist", label.ID)
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) DeleteLabel(labelID string) error {
|
||||
if err := api.checkAndRecordCall(DELETE, "/labels/"+labelID, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
for idx, existingLabel := range api.labels {
|
||||
if existingLabel.ID == labelID {
|
||||
api.labels = append(api.labels[:idx], api.labels[idx+1:]...)
|
||||
api.addEventLabel(pmapi.EventDelete, existingLabel)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("label %s does not exist", labelID)
|
||||
}
|
||||
374
test/fakeapi/messages.go
Normal file
374
test/fakeapi/messages.go
Normal file
@ -0,0 +1,374 @@
|
||||
// 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 fakeapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var errWasNotUpdated = errors.New("message was not updated")
|
||||
|
||||
func (api *FakePMAPI) GetMessage(apiID string) (*pmapi.Message, error) {
|
||||
if err := api.checkAndRecordCall(GET, "/messages/"+apiID, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, message := range api.messages {
|
||||
if message.ID == apiID {
|
||||
return message, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("message %s not found", apiID)
|
||||
}
|
||||
|
||||
// ListMessages does not implement following filters:
|
||||
// * Sort (it sorts by ID only), but Desc works
|
||||
// * Keyword
|
||||
// * To
|
||||
// * Subject
|
||||
// * ID
|
||||
// * Attachments
|
||||
// * AutoWildcard
|
||||
func (api *FakePMAPI) ListMessages(filter *pmapi.MessagesFilter) ([]*pmapi.Message, int, error) {
|
||||
if err := api.checkAndRecordCall(GET, "/messages", filter); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
pageSize := filter.PageSize
|
||||
if pageSize > 150 {
|
||||
pageSize = 150
|
||||
}
|
||||
|
||||
messages := []*pmapi.Message{}
|
||||
messageCount := 0
|
||||
|
||||
skipByIDBegin := true
|
||||
skipByIDEnd := false
|
||||
skipByPaging := pageSize * filter.Page
|
||||
|
||||
for idx := 0; idx < len(api.messages); idx++ {
|
||||
var message *pmapi.Message
|
||||
if !*filter.Desc {
|
||||
message = api.messages[idx]
|
||||
if filter.BeginID == "" || message.ID == filter.BeginID {
|
||||
skipByIDBegin = false
|
||||
}
|
||||
} else {
|
||||
message = api.messages[len(api.messages)-1-idx]
|
||||
if filter.EndID == "" || message.ID == filter.EndID {
|
||||
skipByIDBegin = false
|
||||
}
|
||||
}
|
||||
if skipByIDBegin || skipByIDEnd {
|
||||
continue
|
||||
}
|
||||
if !*filter.Desc {
|
||||
if message.ID == filter.EndID {
|
||||
skipByIDEnd = true
|
||||
}
|
||||
} else {
|
||||
if message.ID == filter.BeginID {
|
||||
skipByIDEnd = true
|
||||
}
|
||||
}
|
||||
if !isMessageMatchingFilter(filter, message) {
|
||||
continue
|
||||
}
|
||||
messageCount++
|
||||
|
||||
if skipByPaging > 0 {
|
||||
skipByPaging--
|
||||
continue
|
||||
}
|
||||
if len(messages) == pageSize || (filter.Limit != 0 && len(messages) == filter.Limit) {
|
||||
continue
|
||||
}
|
||||
messages = append(messages, copyFilteredMessage(message))
|
||||
}
|
||||
|
||||
return messages, messageCount, nil
|
||||
}
|
||||
|
||||
func isMessageMatchingFilter(filter *pmapi.MessagesFilter, message *pmapi.Message) bool {
|
||||
if filter.ExternalID != "" && filter.ExternalID != message.ExternalID {
|
||||
return false
|
||||
}
|
||||
if filter.ConversationID != "" && filter.ConversationID != message.ConversationID {
|
||||
return false
|
||||
}
|
||||
if filter.AddressID != "" && filter.AddressID != message.AddressID {
|
||||
return false
|
||||
}
|
||||
if filter.From != "" && filter.From != message.Sender.Address {
|
||||
return false
|
||||
}
|
||||
if filter.LabelID != "" && !hasItem(message.LabelIDs, filter.LabelID) {
|
||||
return false
|
||||
}
|
||||
if filter.Begin != 0 && filter.Begin > message.Time {
|
||||
return false
|
||||
}
|
||||
if filter.End != 0 && filter.End < message.Time {
|
||||
return false
|
||||
}
|
||||
if filter.Unread != nil {
|
||||
wantUnread := 0
|
||||
if *filter.Unread {
|
||||
wantUnread = 1
|
||||
}
|
||||
if message.Unread != wantUnread {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func copyFilteredMessage(message *pmapi.Message) *pmapi.Message {
|
||||
filteredMessage := &pmapi.Message{}
|
||||
*filteredMessage = *message
|
||||
filteredMessage.Body = ""
|
||||
filteredMessage.Header = nil
|
||||
return filteredMessage
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) CreateDraft(message *pmapi.Message, parentID string, action int) (*pmapi.Message, error) {
|
||||
if err := api.checkAndRecordCall(POST, "/messages", &pmapi.DraftReq{
|
||||
Message: message,
|
||||
ParentID: parentID,
|
||||
Action: action,
|
||||
AttachmentKeyPackets: []string{},
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parentID != "" {
|
||||
if _, err := api.GetMessage(parentID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if message.Subject == "" {
|
||||
message.Subject = "(No Subject)"
|
||||
}
|
||||
message.LabelIDs = append(message.LabelIDs, pmapi.DraftLabel)
|
||||
message.LabelIDs = append(message.LabelIDs, pmapi.AllMailLabel)
|
||||
message.ID = api.controller.messageIDGenerator.next("")
|
||||
api.addMessage(message)
|
||||
return message, nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) SendMessage(messageID string, sendMessageRequest *pmapi.SendMessageReq) (sent, parent *pmapi.Message, err error) {
|
||||
if err := api.checkAndRecordCall(POST, "/messages/"+messageID, sendMessageRequest); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
message, err := api.GetMessage(messageID)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "draft does not exist")
|
||||
}
|
||||
message.Time = time.Now().Unix()
|
||||
message.LabelIDs = append(message.LabelIDs, pmapi.SentLabel)
|
||||
api.addEventMessage(pmapi.EventUpdate, message)
|
||||
return message, nil, nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) Import(importMessageRequests []*pmapi.ImportMsgReq) ([]*pmapi.ImportMsgRes, error) {
|
||||
msgRes := []*pmapi.ImportMsgRes{}
|
||||
for _, msgReq := range importMessageRequests {
|
||||
mailMessage, err := mail.ReadMessage(bytes.NewBuffer(msgReq.Body))
|
||||
if err != nil {
|
||||
msgRes = append(msgRes, &pmapi.ImportMsgRes{
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
messageID := api.controller.messageIDGenerator.next("")
|
||||
message := &pmapi.Message{
|
||||
ID: messageID,
|
||||
AddressID: msgReq.AddressID,
|
||||
Sender: &mail.Address{Address: mailMessage.Header.Get("From")},
|
||||
ToList: []*mail.Address{{Address: mailMessage.Header.Get("To")}},
|
||||
Subject: mailMessage.Header.Get("Subject"),
|
||||
Unread: msgReq.Unread,
|
||||
LabelIDs: msgReq.LabelIDs,
|
||||
Body: string(msgReq.Body),
|
||||
Flags: msgReq.Flags,
|
||||
Time: msgReq.Time,
|
||||
}
|
||||
msgRes = append(msgRes, &pmapi.ImportMsgRes{
|
||||
Error: nil,
|
||||
MessageID: messageID,
|
||||
})
|
||||
api.addMessage(message)
|
||||
}
|
||||
return msgRes, nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) addMessage(message *pmapi.Message) {
|
||||
api.messages = append(api.messages, message)
|
||||
api.addEventMessage(pmapi.EventCreate, message)
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) DeleteMessages(apiIDs []string) error {
|
||||
err := api.deleteMessages(PUT, "/messages/delete", &pmapi.MessagesActionReq{
|
||||
IDs: apiIDs,
|
||||
}, func(message *pmapi.Message) bool {
|
||||
return hasItem(apiIDs, message.ID)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(apiIDs) == 0 {
|
||||
return errBadRequest
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) EmptyFolder(labelID string, addressID string) error {
|
||||
err := api.deleteMessages(DELETE, "/messages/empty?LabelID="+labelID+"&AddressID="+addressID, nil, func(message *pmapi.Message) bool {
|
||||
return hasItem(message.LabelIDs, labelID) && message.AddressID == addressID
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if labelID == "" {
|
||||
return errBadRequest
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) deleteMessages(method method, path string, request interface{}, shouldBeDeleted func(*pmapi.Message) bool) error {
|
||||
if err := api.checkAndRecordCall(method, path, request); err != nil {
|
||||
return err
|
||||
}
|
||||
newMessages := []*pmapi.Message{}
|
||||
for _, message := range api.messages {
|
||||
if shouldBeDeleted(message) {
|
||||
if hasItem(message.LabelIDs, pmapi.TrashLabel) {
|
||||
api.addEventMessage(pmapi.EventDelete, message)
|
||||
continue
|
||||
}
|
||||
message.LabelIDs = []string{pmapi.TrashLabel, pmapi.AllMailLabel}
|
||||
api.addEventMessage(pmapi.EventUpdate, message)
|
||||
}
|
||||
newMessages = append(newMessages, message)
|
||||
}
|
||||
api.messages = newMessages
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) LabelMessages(apiIDs []string, labelID string) error {
|
||||
return api.updateMessages(PUT, "/messages/label", &pmapi.LabelMessagesReq{
|
||||
IDs: apiIDs,
|
||||
LabelID: labelID,
|
||||
}, apiIDs, func(message *pmapi.Message) error {
|
||||
if labelID == "" {
|
||||
return errBadRequest
|
||||
}
|
||||
if labelID == pmapi.TrashLabel {
|
||||
message.LabelIDs = []string{pmapi.TrashLabel, pmapi.AllMailLabel}
|
||||
return nil
|
||||
}
|
||||
if api.isLabelFolder(labelID) {
|
||||
labelIDs := []string{}
|
||||
for _, existingLabelID := range message.LabelIDs {
|
||||
if !api.isLabelFolder(existingLabelID) {
|
||||
labelIDs = append(labelIDs, existingLabelID)
|
||||
}
|
||||
}
|
||||
message.LabelIDs = labelIDs
|
||||
}
|
||||
message.LabelIDs = addItem(message.LabelIDs, labelID)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) UnlabelMessages(apiIDs []string, labelID string) error {
|
||||
return api.updateMessages(PUT, "/messages/unlabel", &pmapi.LabelMessagesReq{
|
||||
IDs: apiIDs,
|
||||
LabelID: labelID,
|
||||
}, apiIDs, func(message *pmapi.Message) error {
|
||||
if labelID == "" {
|
||||
return errBadRequest
|
||||
}
|
||||
// All Mail and Sent cannot be unlabeled, but API will not throw error.
|
||||
if labelID == pmapi.AllMailLabel || labelID == pmapi.SentLabel {
|
||||
return errWasNotUpdated
|
||||
}
|
||||
|
||||
message.LabelIDs = removeItem(message.LabelIDs, labelID)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) MarkMessagesRead(apiIDs []string) error {
|
||||
return api.updateMessages(PUT, "/messages/read", &pmapi.MessagesActionReq{
|
||||
IDs: apiIDs,
|
||||
}, apiIDs, func(message *pmapi.Message) error {
|
||||
if message.Unread == 0 {
|
||||
return errWasNotUpdated
|
||||
}
|
||||
message.Unread = 0
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) MarkMessagesUnread(apiIDs []string) error {
|
||||
err := api.updateMessages(PUT, "/messages/unread", &pmapi.MessagesActionReq{
|
||||
IDs: apiIDs,
|
||||
}, apiIDs, func(message *pmapi.Message) error {
|
||||
if message.Unread == 1 {
|
||||
return errWasNotUpdated
|
||||
}
|
||||
message.Unread = 1
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) updateMessages(method method, path string, request interface{}, apiIDs []string, updateCallback func(*pmapi.Message) error) error { //nolint[unparam]
|
||||
if err := api.checkAndRecordCall(method, path, request); err != nil {
|
||||
return err
|
||||
}
|
||||
// API will return error if you send request for no apiIDs
|
||||
if len(apiIDs) == 0 {
|
||||
return errBadRequest
|
||||
}
|
||||
for _, message := range api.messages {
|
||||
if hasItem(apiIDs, message.ID) {
|
||||
err := updateCallback(message)
|
||||
if err != nil {
|
||||
if err == errWasNotUpdated {
|
||||
continue
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
api.addEventMessage(pmapi.EventUpdate, message)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
44
test/fakeapi/reports.go
Normal file
44
test/fakeapi/reports.go
Normal file
@ -0,0 +1,44 @@
|
||||
// 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 fakeapi
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
|
||||
func (api *FakePMAPI) ReportBugWithEmailClient(os, osVersion, title, description, username, email, emailClient string) error {
|
||||
return api.checkInternetAndRecordCall(POST, "/reports/bug", &pmapi.ReportReq{
|
||||
OS: os,
|
||||
OSVersion: osVersion,
|
||||
Title: title,
|
||||
Description: description,
|
||||
Username: username,
|
||||
Email: email,
|
||||
Browser: emailClient,
|
||||
})
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) SendSimpleMetric(category, action, label string) error {
|
||||
v := url.Values{}
|
||||
v.Set("Category", category)
|
||||
v.Set("Action", action)
|
||||
v.Set("Label", label)
|
||||
return api.checkInternetAndRecordCall(GET, "/metrics?"+v.Encode(), nil)
|
||||
}
|
||||
61
test/fakeapi/user.go
Normal file
61
test/fakeapi/user.go
Normal file
@ -0,0 +1,61 @@
|
||||
// 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 fakeapi
|
||||
|
||||
import (
|
||||
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
|
||||
func (api *FakePMAPI) GetMailSettings() (pmapi.MailSettings, error) {
|
||||
if err := api.checkAndRecordCall(GET, "/settings/mail", nil); err != nil {
|
||||
return pmapi.MailSettings{}, err
|
||||
}
|
||||
return pmapi.MailSettings{}, nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) Unlock(mailboxPassword string) (*pmcrypto.KeyRing, error) {
|
||||
return &pmcrypto.KeyRing{
|
||||
FirstKeyID: "key",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) UnlockAddresses(password []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) CurrentUser() (*pmapi.User, error) {
|
||||
return api.UpdateUser()
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) UpdateUser() (*pmapi.User, error) {
|
||||
if err := api.checkAndRecordCall(GET, "/users", nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return api.user, nil
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) Addresses() pmapi.AddressList {
|
||||
return *api.addresses
|
||||
}
|
||||
|
||||
func (api *FakePMAPI) KeyRingForAddressID(addrID string) *pmcrypto.KeyRing {
|
||||
return &pmcrypto.KeyRing{
|
||||
FirstKeyID: "key",
|
||||
}
|
||||
}
|
||||
46
test/fakeapi/utils.go
Normal file
46
test/fakeapi/utils.go
Normal file
@ -0,0 +1,46 @@
|
||||
// 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 fakeapi
|
||||
|
||||
func hasItem(items []string, value string) bool {
|
||||
for _, item := range items {
|
||||
if item == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func removeItem(items []string, value string) []string {
|
||||
newItems := []string{}
|
||||
for _, item := range items {
|
||||
if item != value {
|
||||
newItems = append(newItems, item)
|
||||
}
|
||||
}
|
||||
return newItems
|
||||
}
|
||||
|
||||
func addItem(items []string, value string) []string {
|
||||
for _, item := range items {
|
||||
if item == value {
|
||||
return items
|
||||
}
|
||||
}
|
||||
return append(items, value)
|
||||
}
|
||||
46
test/features/bridge/addressmode.feature
Normal file
46
test/features/bridge/addressmode.feature
Normal file
@ -0,0 +1,46 @@
|
||||
Feature: Address mode
|
||||
Background:
|
||||
Given there is connected user "userMoreAddresses"
|
||||
And there is "userMoreAddresses" with mailbox "Folders/mbox"
|
||||
And there are messages in mailbox "Folders/mbox" for "userMoreAddresses"
|
||||
| from | to | subject |
|
||||
| john.doe@mail.com | [primary] | foo |
|
||||
| jane.doe@mail.com | [secondary] | bar |
|
||||
|
||||
Scenario: All messages in one mailbox with combined mode
|
||||
Given there is "userMoreAddresses" in "combined" address mode
|
||||
Then mailbox "Folders/mbox" for address "primary" of "userMoreAddresses" has messages
|
||||
| from | to | subject |
|
||||
| john.doe@mail.com | [primary] | foo |
|
||||
| jane.doe@mail.com | [secondary] | bar |
|
||||
|
||||
Scenario: Messages separated in more mailboxes with split mode
|
||||
Given there is "userMoreAddresses" in "split" address mode
|
||||
Then mailbox "Folders/mbox" for address "primary" of "userMoreAddresses" has messages
|
||||
| from | to | subject |
|
||||
| john.doe@mail.com | [primary] | foo |
|
||||
And mailbox "Folders/mbox" for address "secondary" of "userMoreAddresses" has messages
|
||||
| from | to | subject |
|
||||
| jane.doe@mail.com | [secondary] | bar |
|
||||
|
||||
Scenario: Switch address mode from combined to split mode
|
||||
Given there is "userMoreAddresses" in "combined" address mode
|
||||
When "userMoreAddresses" changes the address mode
|
||||
Then bridge response is "OK"
|
||||
And "userMoreAddresses" has address mode in "split" mode
|
||||
And mailbox "Folders/mbox" for address "primary" of "userMoreAddresses" has messages
|
||||
| from | to | subject |
|
||||
| john.doe@mail.com | [primary] | foo |
|
||||
And mailbox "Folders/mbox" for address "secondary" of "userMoreAddresses" has messages
|
||||
| from | to | subject |
|
||||
| jane.doe@mail.com | [secondary] | bar |
|
||||
|
||||
Scenario: Switch address mode from split to combined mode
|
||||
Given there is "userMoreAddresses" in "split" address mode
|
||||
When "userMoreAddresses" changes the address mode
|
||||
Then bridge response is "OK"
|
||||
And "userMoreAddresses" has address mode in "combined" mode
|
||||
And mailbox "Folders/mbox" for address "primary" of "userMoreAddresses" has messages
|
||||
| from | to | subject |
|
||||
| john.doe@mail.com | [primary] | foo |
|
||||
| jane.doe@mail.com | [secondary] | bar |
|
||||
36
test/features/bridge/deleteuser.feature
Normal file
36
test/features/bridge/deleteuser.feature
Normal file
@ -0,0 +1,36 @@
|
||||
Feature: Delete user
|
||||
Scenario: Deleting connected user
|
||||
Given there is connected user "user"
|
||||
When user deletes "user" from bridge
|
||||
Then bridge response is "OK"
|
||||
And "user" has database file
|
||||
|
||||
Scenario: Deleting connected user with cache
|
||||
Given there is connected user "user"
|
||||
When user deletes "user" from bridge with cache
|
||||
Then bridge response is "OK"
|
||||
And "user" does not have database file
|
||||
|
||||
Scenario: Deleting connected user without database file
|
||||
Given there is connected user "user"
|
||||
And there is no database file for "user"
|
||||
When user deletes "user" from bridge with cache
|
||||
Then bridge response is "OK"
|
||||
|
||||
Scenario: Deleting disconnected user
|
||||
Given there is disconnected user "user"
|
||||
When user deletes "user" from bridge
|
||||
Then bridge response is "OK"
|
||||
And "user" has database file
|
||||
|
||||
Scenario: Deleting disconnected user with cache
|
||||
Given there is disconnected user "user"
|
||||
When user deletes "user" from bridge with cache
|
||||
Then bridge response is "OK"
|
||||
And "user" does not have database file
|
||||
|
||||
Scenario: Deleting disconnected user without database file
|
||||
Given there is disconnected user "user"
|
||||
And there is no database file for "user"
|
||||
When user deletes "user" from bridge with cache
|
||||
Then bridge response is "OK"
|
||||
66
test/features/bridge/login.feature
Normal file
66
test/features/bridge/login.feature
Normal file
@ -0,0 +1,66 @@
|
||||
Feature: Login to bridge for the first time
|
||||
Scenario: Normal bridge login
|
||||
Given there is user "user"
|
||||
When "user" logs in to bridge
|
||||
Then bridge response is "OK"
|
||||
And "user" is connected
|
||||
And "user" has database file
|
||||
And "user" has running event loop
|
||||
|
||||
Scenario: Login with bad username
|
||||
When "user" logs in to bridge with bad password
|
||||
Then bridge response is "failed to login: Incorrect login credentials. Please try again"
|
||||
|
||||
Scenario: Login with bad password
|
||||
Given there is user "user"
|
||||
When "user" logs in to bridge with bad password
|
||||
Then bridge response is "failed to login: Incorrect login credentials. Please try again"
|
||||
|
||||
Scenario: Login without internet connection
|
||||
Given there is no internet connection
|
||||
When "user" logs in to bridge
|
||||
Then bridge response is "failed to login: cannot reach the server"
|
||||
|
||||
@ignore-live
|
||||
Scenario: Login user with 2FA
|
||||
Given there is user "user2fa"
|
||||
When "user2fa" logs in to bridge
|
||||
Then bridge response is "OK"
|
||||
And "user2fa" is connected
|
||||
And "user2fa" has database file
|
||||
And "user2fa" has running event loop
|
||||
|
||||
Scenario: Login user with capital letters in address
|
||||
Given there is user "userAddressWithCapitalLetter"
|
||||
When "userAddressWithCapitalLetter" logs in to bridge
|
||||
Then bridge response is "OK"
|
||||
And "userAddressWithCapitalLetter" is connected
|
||||
And "userAddressWithCapitalLetter" has database file
|
||||
And "userAddressWithCapitalLetter" has running event loop
|
||||
|
||||
Scenario: Login user with more addresses
|
||||
Given there is user "userMoreAddresses"
|
||||
When "userMoreAddresses" logs in to bridge
|
||||
Then bridge response is "OK"
|
||||
And "userMoreAddresses" is connected
|
||||
And "userMoreAddresses" has database file
|
||||
And "userMoreAddresses" has running event loop
|
||||
|
||||
@ignore-live
|
||||
Scenario: Login user with disabled primary address
|
||||
Given there is user "userDisabledPrimaryAddress"
|
||||
When "userDisabledPrimaryAddress" logs in to bridge
|
||||
Then bridge response is "OK"
|
||||
And "userDisabledPrimaryAddress" is connected
|
||||
And "userDisabledPrimaryAddress" has database file
|
||||
And "userDisabledPrimaryAddress" has running event loop
|
||||
|
||||
Scenario: Login two users
|
||||
Given there is user "user"
|
||||
And there is user "userMoreAddresses"
|
||||
When "user" logs in to bridge
|
||||
Then bridge response is "OK"
|
||||
And "user" is connected
|
||||
When "userMoreAddresses" logs in to bridge
|
||||
Then bridge response is "OK"
|
||||
And "userMoreAddresses" is connected
|
||||
35
test/features/bridge/relogin.feature
Normal file
35
test/features/bridge/relogin.feature
Normal file
@ -0,0 +1,35 @@
|
||||
Feature: Re-login to bridge
|
||||
Scenario: Re-login with connected user and database file
|
||||
Given there is connected user "user"
|
||||
And there is database file for "user"
|
||||
When "user" logs in to bridge
|
||||
Then bridge response is "failed to finish login: user is already logged in"
|
||||
And "user" is connected
|
||||
And "user" has running event loop
|
||||
|
||||
@ignore
|
||||
Scenario: Re-login with connected user and no database file
|
||||
Given there is connected user "user"
|
||||
And there is no database file for "user"
|
||||
When "user" logs in to bridge
|
||||
Then bridge response is "failed to finish login: user is already logged in"
|
||||
And "user" is connected
|
||||
And "user" has database file
|
||||
And "user" has running event loop
|
||||
|
||||
Scenario: Re-login with disconnected user and database file
|
||||
Given there is disconnected user "user"
|
||||
And there is database file for "user"
|
||||
When "user" logs in to bridge
|
||||
Then bridge response is "OK"
|
||||
And "user" is connected
|
||||
And "user" has running event loop
|
||||
|
||||
Scenario: Re-login with disconnected user and no database file
|
||||
Given there is disconnected user "user"
|
||||
And there is no database file for "user"
|
||||
When "user" logs in to bridge
|
||||
Then bridge response is "OK"
|
||||
And "user" is connected
|
||||
And "user" has database file
|
||||
And "user" has running event loop
|
||||
78
test/features/bridge/start.feature
Normal file
78
test/features/bridge/start.feature
Normal file
@ -0,0 +1,78 @@
|
||||
Feature: Start bridge
|
||||
Scenario: Start with connected user, database file and internet connection
|
||||
Given there is connected user "user"
|
||||
And there is database file for "user"
|
||||
When bridge starts
|
||||
Then "user" is connected
|
||||
And "user" has loaded store
|
||||
And "user" has running event loop
|
||||
And "user" has API auth
|
||||
|
||||
Scenario: Start with connected user, database file and no internet connection
|
||||
Given there is connected user "user"
|
||||
And there is database file for "user"
|
||||
And there is no internet connection
|
||||
When bridge starts
|
||||
Then "user" is connected
|
||||
And "user" has loaded store
|
||||
And "user" has running event loop
|
||||
And "user" does not have API auth
|
||||
|
||||
@ignore
|
||||
Scenario: Start with connected user, no database file and internet connection
|
||||
Given there is connected user "user"
|
||||
And there is no database file for "user"
|
||||
When bridge starts
|
||||
Then "user" is connected
|
||||
And "user" has loaded store
|
||||
And "user" has running event loop
|
||||
And "user" has API auth
|
||||
|
||||
@ignore
|
||||
Scenario: Start with connected user, no database file and no internet connection
|
||||
Given there is connected user "user"
|
||||
And there is no database file for "user"
|
||||
And there is no internet connection
|
||||
When bridge starts
|
||||
Then "user" is disconnected
|
||||
And "user" does not have API auth
|
||||
|
||||
Scenario: Start with disconnected user, database file and internet connection
|
||||
Given there is disconnected user "user"
|
||||
And there is database file for "user"
|
||||
When bridge starts
|
||||
Then "user" is disconnected
|
||||
And "user" has loaded store
|
||||
And "user" does not have running event loop
|
||||
And "user" does not have API auth
|
||||
|
||||
Scenario: Start with disconnected user, database file and no internet connection
|
||||
Given there is disconnected user "user"
|
||||
And there is database file for "user"
|
||||
And there is no internet connection
|
||||
When bridge starts
|
||||
Then "user" is disconnected
|
||||
And "user" has loaded store
|
||||
And "user" does not have running event loop
|
||||
And "user" does not have API auth
|
||||
|
||||
@ignore
|
||||
Scenario: Start with disconnected user, no database file and internet connection
|
||||
Given there is disconnected user "user"
|
||||
And there is no database file for "user"
|
||||
When bridge starts
|
||||
Then "user" is disconnected
|
||||
And "user" does not have loaded store
|
||||
And "user" does not have running event loop
|
||||
And "user" does not have API auth
|
||||
|
||||
@ignore
|
||||
Scenario: Start with disconnected user, no database file and no internet connection
|
||||
Given there is disconnected user "user"
|
||||
And there is no database file for "user"
|
||||
And there is no internet connection
|
||||
When bridge starts
|
||||
Then "user" is disconnected
|
||||
And "user" does not have loaded store
|
||||
And "user" does not have running event loop
|
||||
And "user" does not have API auth
|
||||
60
test/features/bridge/sync.feature
Normal file
60
test/features/bridge/sync.feature
Normal file
@ -0,0 +1,60 @@
|
||||
Feature: Sync bridge
|
||||
Background:
|
||||
Given there is connected user "userMoreAddresses"
|
||||
And there is "userMoreAddresses" with mailboxes
|
||||
| Folders/one |
|
||||
| Folders/two |
|
||||
| Labels/three |
|
||||
| Labels/four |
|
||||
And there are messages for "userMoreAddresses" as follows
|
||||
| address | mailboxes | messages |
|
||||
| primary | INBOX,Folders/one | 1 |
|
||||
| primary | Archive,Labels/three | 2 |
|
||||
| primary | INBOX | 1000 |
|
||||
| primary | Archive | 1200 |
|
||||
| primary | Folders/one | 1400 |
|
||||
| primary | Folders/two | 1600 |
|
||||
| primary | Labels/three | 1800 |
|
||||
| primary | Labels/four | 2000 |
|
||||
| secondary | INBOX | 100 |
|
||||
| secondary | Archive | 120 |
|
||||
| secondary | Folders/one | 140 |
|
||||
| secondary | Folders/two | 160 |
|
||||
| secondary | Labels/three | 180 |
|
||||
| secondary | Labels/four | 200 |
|
||||
|
||||
# Too heavy for live.
|
||||
@ignore-live
|
||||
Scenario: Sync in combined mode
|
||||
And there is "userMoreAddresses" in "combined" address mode
|
||||
When bridge syncs "userMoreAddresses"
|
||||
Then bridge response is "OK"
|
||||
And "userMoreAddresses" has the following messages
|
||||
| mailboxes | messages |
|
||||
| INBOX | 1101 |
|
||||
| Archive | 1322 |
|
||||
| Folders/one | 1541 |
|
||||
| Folders/two | 1760 |
|
||||
| Labels/three | 1982 |
|
||||
| Labels/four | 2200 |
|
||||
|
||||
# Too heavy for live.
|
||||
@ignore-live
|
||||
Scenario: Sync in split mode
|
||||
And there is "userMoreAddresses" in "split" address mode
|
||||
When bridge syncs "userMoreAddresses"
|
||||
Then bridge response is "OK"
|
||||
And "userMoreAddresses" has the following messages
|
||||
| address | mailboxes | messages |
|
||||
| primary | INBOX | 1001 |
|
||||
| primary | Archive | 1202 |
|
||||
| primary | Folders/one | 1401 |
|
||||
| primary | Folders/two | 1600 |
|
||||
| primary | Labels/three | 1802 |
|
||||
| primary | Labels/four | 2000 |
|
||||
| secondary | INBOX | 100 |
|
||||
| secondary | Archive | 120 |
|
||||
| secondary | Folders/one | 140 |
|
||||
| secondary | Folders/two | 160 |
|
||||
| secondary | Labels/three | 180 |
|
||||
| secondary | Labels/four | 200 |
|
||||
88
test/features/imap/auth.feature
Normal file
88
test/features/imap/auth.feature
Normal file
@ -0,0 +1,88 @@
|
||||
Feature: IMAP auth
|
||||
Scenario: Authenticates successfully
|
||||
Given there is connected user "user"
|
||||
When IMAP client authenticates "user"
|
||||
Then IMAP response is "OK"
|
||||
|
||||
Scenario: Authenticates with bad password
|
||||
Given there is connected user "user"
|
||||
When IMAP client authenticates "user" with bad password
|
||||
Then IMAP response is "IMAP error: NO backend/credentials: incorrect password"
|
||||
|
||||
Scenario: Authenticates with disconnected user
|
||||
Given there is disconnected user "user"
|
||||
When IMAP client authenticates "user"
|
||||
Then IMAP response is "IMAP error: NO bridge account is logged out, use bridge to login again"
|
||||
|
||||
Scenario: Authenticates with connected user that was loaded without internet
|
||||
Given there is connected user "user"
|
||||
And there is no internet connection
|
||||
When bridge starts
|
||||
And the internet connection is restored
|
||||
And the event loop of "user" loops once
|
||||
And IMAP client authenticates "user"
|
||||
# Problems during IMAP auth could lead to the user being disconnected.
|
||||
# This could take a few milliseconds because it happens async in separate goroutines.
|
||||
# We wait enough time for that to happen, then check that it didn't happen (user should remain connected).
|
||||
And 2 seconds pass
|
||||
Then "user" is connected
|
||||
|
||||
Scenario: Authenticates with freshly logged-out user
|
||||
Given there is connected user "user"
|
||||
When "user" logs out from bridge
|
||||
And IMAP client authenticates "user"
|
||||
Then IMAP response is "IMAP error: NO bridge account is logged out, use bridge to login again"
|
||||
|
||||
Scenario: Authenticates user which was re-logged in
|
||||
Given there is connected user "user"
|
||||
When "user" logs out from bridge
|
||||
And IMAP client authenticates "user"
|
||||
Then IMAP response is "IMAP error: NO bridge account is logged out, use bridge to login again"
|
||||
When "user" logs in to bridge
|
||||
And IMAP client authenticates "user"
|
||||
Then IMAP response is "OK"
|
||||
When IMAP client selects "INBOX"
|
||||
Then IMAP response is "OK"
|
||||
|
||||
Scenario: Authenticates with no user
|
||||
When IMAP client authenticates with username "user@pm.me" and password "bridgepassword"
|
||||
Then IMAP response is "IMAP error: NO user user@pm.me not found"
|
||||
|
||||
Scenario: Authenticates with capital letter
|
||||
Given there is connected user "userAddressWithCapitalLetter"
|
||||
When IMAP client authenticates "userAddressWithCapitalLetter"
|
||||
Then IMAP response is "OK"
|
||||
|
||||
Scenario: Authenticates with more addresses - primary one
|
||||
Given there is connected user "userMoreAddresses"
|
||||
When IMAP client authenticates "userMoreAddresses" with address "primary"
|
||||
Then IMAP response is "OK"
|
||||
|
||||
Scenario: Authenticates with more addresses - secondary one
|
||||
Given there is connected user "userMoreAddresses"
|
||||
When IMAP client authenticates "userMoreAddresses" with address "secondary"
|
||||
Then IMAP response is "OK"
|
||||
|
||||
Scenario: Authenticates with more addresses - disabled address
|
||||
Given there is connected user "userMoreAddresses"
|
||||
When IMAP client authenticates "userMoreAddresses" with address "disabled"
|
||||
Then IMAP response is "IMAP error: NO user .* not found"
|
||||
|
||||
@ignore-live
|
||||
Scenario: Authenticates with disabled primary address
|
||||
Given there is connected user "userDisabledPrimaryAddress"
|
||||
When IMAP client authenticates "userDisabledPrimaryAddress" with address "primary"
|
||||
Then IMAP response is "OK"
|
||||
|
||||
Scenario: Authenticates two users
|
||||
Given there is connected user "user"
|
||||
And there is connected user "userMoreAddresses"
|
||||
When IMAP client "imap1" authenticates "user"
|
||||
Then IMAP response to "imap1" is "OK"
|
||||
When IMAP client "imap2" authenticates "userMoreAddresses" with address "primary"
|
||||
Then IMAP response to "imap2" is "OK"
|
||||
|
||||
Scenario: Logs out user
|
||||
Given there is connected user "user"
|
||||
When IMAP client logs out
|
||||
Then IMAP response is "OK"
|
||||
62
test/features/imap/idle/basic.feature
Normal file
62
test/features/imap/idle/basic.feature
Normal file
@ -0,0 +1,62 @@
|
||||
Feature: IMAP IDLE
|
||||
Background:
|
||||
Given there is connected user "user"
|
||||
And there are 10 messages in mailbox "INBOX" for "user"
|
||||
|
||||
@ignore
|
||||
Scenario Outline: Mark as read
|
||||
Given there is IMAP client "active" logged in as "user"
|
||||
And there is IMAP client "active" selected in "INBOX"
|
||||
And there is IMAP client "idling" logged in as "user"
|
||||
And there is IMAP client "idling" selected in "INBOX"
|
||||
When IMAP client "idling" starts IDLE-ing
|
||||
And IMAP client "active" marks message "<message>" as read
|
||||
Then IMAP client "idling" receives update marking message "<message>" as read within <seconds> seconds
|
||||
Then message "<message>" in "INBOX" for "user" is marked as read
|
||||
|
||||
Examples:
|
||||
| message | seconds |
|
||||
| 1 | 2 |
|
||||
| 1:5 | 2 |
|
||||
| 1:10 | 5 |
|
||||
|
||||
@ignore
|
||||
Scenario Outline: Mark as unread
|
||||
Given there is IMAP client "active" logged in as "user"
|
||||
And there is IMAP client "active" selected in "INBOX"
|
||||
And there is IMAP client "idling" logged in as "user"
|
||||
And there is IMAP client "idling" selected in "INBOX"
|
||||
When IMAP client "idling" starts IDLE-ing
|
||||
And IMAP client "active" marks message "<message>" as unread
|
||||
Then IMAP client "idling" receives update marking message "<message>" as unread within <seconds> seconds
|
||||
And message "<message>" in "INBOX" for "user" is marked as unread
|
||||
|
||||
Examples:
|
||||
| message | seconds |
|
||||
| 1 | 2 |
|
||||
| 1:5 | 2 |
|
||||
| 1:10 | 5 |
|
||||
|
||||
@ignore
|
||||
Scenario Outline: Three IDLEing
|
||||
Given there is IMAP client "active" logged in as "user"
|
||||
And there is IMAP client "active" selected in "INBOX"
|
||||
And there is IMAP client "idling1" logged in as "user"
|
||||
And there is IMAP client "idling1" selected in "INBOX"
|
||||
And there is IMAP client "idling2" logged in as "user"
|
||||
And there is IMAP client "idling2" selected in "INBOX"
|
||||
And there is IMAP client "idling3" logged in as "user"
|
||||
And there is IMAP client "idling3" selected in "INBOX"
|
||||
When IMAP client "idling1" starts IDLE-ing
|
||||
And IMAP client "idling2" starts IDLE-ing
|
||||
And IMAP client "idling3" starts IDLE-ing
|
||||
And IMAP client "active" marks message "<message>" as read
|
||||
Then IMAP client "idling1" receives update marking message "<message>" as read within <seconds> seconds
|
||||
Then IMAP client "idling2" receives update marking message "<message>" as read within <seconds> seconds
|
||||
Then IMAP client "idling3" receives update marking message "<message>" as read within <seconds> seconds
|
||||
|
||||
Examples:
|
||||
| message | seconds |
|
||||
| 1 | 2 |
|
||||
| 1:5 | 2 |
|
||||
| 1:10 | 5 |
|
||||
28
test/features/imap/idle/two_users.feature
Normal file
28
test/features/imap/idle/two_users.feature
Normal file
@ -0,0 +1,28 @@
|
||||
Feature: IMAP IDLE with two users
|
||||
Scenario: IDLE statements are not leaked to other account
|
||||
Given there is connected user "user"
|
||||
And there are 10 messages in mailbox "INBOX" for "user"
|
||||
And there is connected user "userMoreAddresses"
|
||||
And there is IMAP client "active" logged in as "user"
|
||||
And there is IMAP client "active" selected in "INBOX"
|
||||
And there is IMAP client "idling" logged in as "userMoreAddresses"
|
||||
And there is IMAP client "idling" selected in "INBOX"
|
||||
When IMAP client "idling" starts IDLE-ing
|
||||
And IMAP client "active" marks message "1" as read
|
||||
Then IMAP client "idling" does not receive update for message "1" within 5 seconds
|
||||
|
||||
Scenario: IDLE statements are not leaked to other alias
|
||||
Given there is connected user "userMoreAddresses"
|
||||
And there is "userMoreAddresses" in "combined" address mode
|
||||
And there is "userMoreAddresses" with mailbox "Folders/mbox"
|
||||
And there are messages in mailbox "Folders/mbox" for "userMoreAddresses"
|
||||
| from | to | subject |
|
||||
| john.doe@mail.com | [primary] | foo |
|
||||
| jane.doe@mail.com | [secondary] | bar |
|
||||
And there is IMAP client "active" logged in as "userMoreAddresses" with address "primary"
|
||||
And there is IMAP client "active" selected in "INBOX"
|
||||
And there is IMAP client "idling" logged in as "userMoreAddresses" with address "secondary"
|
||||
And there is IMAP client "idling" selected in "INBOX"
|
||||
When IMAP client "idling" starts IDLE-ing
|
||||
And IMAP client "active" marks message "1" as read
|
||||
Then IMAP client "idling" does not receive update for message "1" within 5 seconds
|
||||
25
test/features/imap/mailbox/create.feature
Normal file
25
test/features/imap/mailbox/create.feature
Normal file
@ -0,0 +1,25 @@
|
||||
Feature: IMAP create mailbox
|
||||
Background:
|
||||
Given there is connected user "user"
|
||||
And there is IMAP client logged in as "user"
|
||||
|
||||
Scenario: Create folder
|
||||
When IMAP client creates mailbox "Folders/mbox"
|
||||
Then IMAP response is "OK"
|
||||
And "user" has mailbox "Folders/mbox"
|
||||
And "user" does not have mailbox "Labels/mbox"
|
||||
|
||||
Scenario: Create label
|
||||
When IMAP client creates mailbox "Labels/mbox"
|
||||
Then IMAP response is "OK"
|
||||
And "user" does not have mailbox "Folders/mbox"
|
||||
And "user" has mailbox "Labels/mbox"
|
||||
|
||||
Scenario: Creating system mailbox is not possible
|
||||
When IMAP client creates mailbox "INBOX"
|
||||
Then IMAP response is "IMAP error: NO mailbox INBOX already exists"
|
||||
|
||||
Scenario: Creating mailbox without prefix is not possible
|
||||
When IMAP client creates mailbox "mbox"
|
||||
Then IMAP response is "OK"
|
||||
And "user" does not have mailbox "mbox"
|
||||
29
test/features/imap/mailbox/delete.feature
Normal file
29
test/features/imap/mailbox/delete.feature
Normal file
@ -0,0 +1,29 @@
|
||||
Feature: IMAP delete mailbox
|
||||
Background:
|
||||
Given there is connected user "user"
|
||||
|
||||
Scenario: Delete folder
|
||||
Given there is "user" with mailbox "Folders/mbox"
|
||||
And there is IMAP client logged in as "user"
|
||||
When IMAP client deletes mailbox "Folders/mbox"
|
||||
Then IMAP response is "OK"
|
||||
And "user" does not have mailbox "Folders/mbox"
|
||||
|
||||
Scenario: Delete label
|
||||
Given there is "user" with mailbox "Labels/mbox"
|
||||
And there is IMAP client logged in as "user"
|
||||
When IMAP client deletes mailbox "Labels/mbox"
|
||||
Then IMAP response is "OK"
|
||||
And "user" does not have mailbox "Labels/mbox"
|
||||
|
||||
Scenario: Empty Trash by deleting it
|
||||
Given there are 10 messages in mailbox "Trash" for "user"
|
||||
And there is IMAP client logged in as "user"
|
||||
When IMAP client deletes mailbox "Trash"
|
||||
Then IMAP response is "OK"
|
||||
And mailbox "Trash" for "user" has 0 messages
|
||||
|
||||
Scenario: Deleting system mailbox is not possible
|
||||
Given there is IMAP client logged in as "user"
|
||||
When IMAP client deletes mailbox "INBOX"
|
||||
Then IMAP response is "IMAP error: NO cannot empty mailbox 0"
|
||||
15
test/features/imap/mailbox/info.feature
Normal file
15
test/features/imap/mailbox/info.feature
Normal file
@ -0,0 +1,15 @@
|
||||
Feature: IMAP get mailbox info
|
||||
Background:
|
||||
Given there is connected user "user"
|
||||
And there are messages in mailbox "INBOX" for "user"
|
||||
| from | to | subject | body | read | starred |
|
||||
| john.doe@mail.com | user@pm.me | foo | hello | false | false |
|
||||
| jane.doe@mail.com | name@pm.me | bar | world | true | true |
|
||||
And there is IMAP client logged in as "user"
|
||||
|
||||
Scenario: Mailbox info contains mailbox name
|
||||
When IMAP client gets info of "INBOX"
|
||||
Then IMAP response contains "2 EXISTS"
|
||||
And IMAP response contains "UNSEEN 1"
|
||||
And IMAP response contains "UIDNEXT 3"
|
||||
And IMAP response contains "UIDVALIDITY"
|
||||
16
test/features/imap/mailbox/list.feature
Normal file
16
test/features/imap/mailbox/list.feature
Normal file
@ -0,0 +1,16 @@
|
||||
Feature: IMAP list mailboxes
|
||||
Background:
|
||||
Given there is connected user "user"
|
||||
And there is "user" with mailbox "Folders/mbox1"
|
||||
And there is "user" with mailbox "Labels/mbox2"
|
||||
And there is IMAP client logged in as "user"
|
||||
|
||||
Scenario: List mailboxes
|
||||
When IMAP client lists mailboxes
|
||||
Then IMAP response contains "INBOX"
|
||||
Then IMAP response contains "Sent"
|
||||
Then IMAP response contains "Archive"
|
||||
Then IMAP response contains "Trash"
|
||||
Then IMAP response contains "All Mail"
|
||||
Then IMAP response contains "Folders/mbox1"
|
||||
Then IMAP response contains "Labels/mbox2"
|
||||
30
test/features/imap/mailbox/rename.feature
Normal file
30
test/features/imap/mailbox/rename.feature
Normal file
@ -0,0 +1,30 @@
|
||||
Feature: IMAP mailbox rename
|
||||
Background:
|
||||
Given there is connected user "user"
|
||||
|
||||
Scenario: Rename folder
|
||||
Given there is "user" with mailbox "Folders/mbox"
|
||||
And there is IMAP client logged in as "user"
|
||||
When IMAP client renames mailbox "Folders/mbox" to "Folders/mbox2"
|
||||
Then IMAP response is "OK"
|
||||
And "user" does not have mailbox "Folders/mbox"
|
||||
And "user" has mailbox "Folders/mbox2"
|
||||
|
||||
Scenario: Rename label
|
||||
Given there is "user" with mailbox "Labels/mbox"
|
||||
And there is IMAP client logged in as "user"
|
||||
When IMAP client renames mailbox "Labels/mbox" to "Labels/mbox2"
|
||||
Then IMAP response is "OK"
|
||||
And "user" does not have mailbox "Labels/mbox"
|
||||
And "user" has mailbox "Labels/mbox2"
|
||||
|
||||
Scenario: Renaming folder to label is not possible
|
||||
Given there is "user" with mailbox "Folders/mbox"
|
||||
And there is IMAP client logged in as "user"
|
||||
When IMAP client renames mailbox "Folders/mbox" to "Labels/mbox"
|
||||
Then IMAP response is "IMAP error: NO cannot rename folder to non-folder"
|
||||
|
||||
Scenario: Renaming system folder is not possible
|
||||
Given there is IMAP client logged in as "user"
|
||||
When IMAP client renames mailbox "INBOX" to "Folders/mbox"
|
||||
Then IMAP response is "IMAP error: NO cannot rename system mailboxes"
|
||||
17
test/features/imap/mailbox/select.feature
Normal file
17
test/features/imap/mailbox/select.feature
Normal file
@ -0,0 +1,17 @@
|
||||
Feature: IMAP select into mailbox
|
||||
Background:
|
||||
Given there is connected user "user"
|
||||
And there is "user" with mailbox "Folders/mbox"
|
||||
And there is IMAP client logged in as "user"
|
||||
|
||||
Scenario: Select into inbox
|
||||
When IMAP client selects "INBOX"
|
||||
Then IMAP response is "OK"
|
||||
|
||||
Scenario: Select into custom mailbox
|
||||
When IMAP client selects "Folders/mbox"
|
||||
Then IMAP response is "OK"
|
||||
|
||||
Scenario: Select into non-existing mailbox
|
||||
When IMAP client selects "qwerty"
|
||||
Then IMAP response is "IMAP error: NO mailbox qwerty does not exist"
|
||||
21
test/features/imap/mailbox/status.feature
Normal file
21
test/features/imap/mailbox/status.feature
Normal file
@ -0,0 +1,21 @@
|
||||
Feature: IMAP get mailbox status
|
||||
Background:
|
||||
Given there is connected user "user"
|
||||
And there are messages in mailbox "INBOX" for "user"
|
||||
| from | to | subject | body | read | starred |
|
||||
| john.doe@mail.com | user@pm.me | foo | hello | false | false |
|
||||
| jane.doe@mail.com | name@pm.me | bar | world | true | true |
|
||||
And there is IMAP client logged in as "user"
|
||||
And there is IMAP client selected in "INBOX"
|
||||
|
||||
Scenario: Mailbox status contains mailbox name
|
||||
When IMAP client gets status of "INBOX"
|
||||
Then IMAP response contains "INBOX"
|
||||
|
||||
Scenario: Mailbox status contains
|
||||
When IMAP client gets status of "INBOX"
|
||||
Then IMAP response contains "INBOX"
|
||||
And IMAP response contains "MESSAGES 2"
|
||||
And IMAP response contains "UNSEEN 1"
|
||||
And IMAP response contains "UIDNEXT 3"
|
||||
And IMAP response contains "UIDVALIDITY"
|
||||
45
test/features/imap/message/copy.feature
Normal file
45
test/features/imap/message/copy.feature
Normal file
@ -0,0 +1,45 @@
|
||||
Feature: IMAP copy messages
|
||||
Background:
|
||||
Given there is connected user "user"
|
||||
And there is "user" with mailbox "Folders/mbox"
|
||||
And there is "user" with mailbox "Labels/label"
|
||||
And there are messages in mailbox "INBOX" for "user"
|
||||
| from | to | subject | body |
|
||||
| john.doe@mail.com | user@pm.me | foo | hello |
|
||||
| jane.doe@mail.com | name@pm.me | bar | world |
|
||||
And there is IMAP client logged in as "user"
|
||||
And there is IMAP client selected in "INBOX"
|
||||
|
||||
Scenario: Copy message to label
|
||||
When IMAP client copies messages "2" to "Labels/label"
|
||||
Then IMAP response is "OK"
|
||||
And mailbox "INBOX" for "user" has 2 messages
|
||||
And mailbox "Labels/label" for "user" has messages
|
||||
| from | to | subject |
|
||||
| john.doe@mail.com | user@pm.me | foo |
|
||||
|
||||
Scenario: Copy all messages to label
|
||||
When IMAP client copies messages "1:*" to "Labels/label"
|
||||
Then IMAP response is "OK"
|
||||
And mailbox "INBOX" for "user" has 2 messages
|
||||
And mailbox "Labels/label" for "user" has messages
|
||||
| from | to | subject |
|
||||
| john.doe@mail.com | user@pm.me | foo |
|
||||
| jane.doe@mail.com | name@pm.me | bar |
|
||||
|
||||
Scenario: Copy message to folder does move
|
||||
When IMAP client copies messages "2" to "Folders/mbox"
|
||||
Then IMAP response is "OK"
|
||||
And mailbox "INBOX" for "user" has 1 message
|
||||
And mailbox "Folders/mbox" for "user" has messages
|
||||
| from | to | subject |
|
||||
| john.doe@mail.com | user@pm.me | foo |
|
||||
|
||||
Scenario: Copy all messages to folder does move
|
||||
When IMAP client copies messages "1:*" to "Folders/mbox"
|
||||
Then IMAP response is "OK"
|
||||
And mailbox "INBOX" for "user" has 0 messages
|
||||
And mailbox "Folders/mbox" for "user" has messages
|
||||
| from | to | subject |
|
||||
| john.doe@mail.com | user@pm.me | foo |
|
||||
| jane.doe@mail.com | name@pm.me | bar |
|
||||
59
test/features/imap/message/create.feature
Normal file
59
test/features/imap/message/create.feature
Normal file
@ -0,0 +1,59 @@
|
||||
Feature: IMAP create messages
|
||||
Background:
|
||||
Given there is connected user "userMoreAddresses"
|
||||
And there is IMAP client logged in as "userMoreAddresses"
|
||||
|
||||
Scenario: Creates message to user's primary address
|
||||
Given there is IMAP client selected in "INBOX"
|
||||
When IMAP client creates message "foo" from "john.doe@email.com" to address "primary" of "userMoreAddresses" with body "hello world" in "INBOX"
|
||||
Then IMAP response is "OK"
|
||||
And mailbox "INBOX" for "userMoreAddresses" has messages
|
||||
| from | to | subject | read |
|
||||
| john.doe@email.com | [primary] | foo | true |
|
||||
|
||||
Scenario: Creates draft
|
||||
When IMAP client creates message "foo" from address "primary" of "userMoreAddresses" to "john.doe@email.com" with body "hello world" in "Drafts"
|
||||
Then IMAP response is "OK"
|
||||
And mailbox "Drafts" for "userMoreAddresses" has messages
|
||||
| from | to | subject | read |
|
||||
| [primary] | john.doe@email.com | foo | true |
|
||||
|
||||
@ignore
|
||||
Scenario: Creates message sent from user's primary address
|
||||
Given there is IMAP client selected in "Sent"
|
||||
When IMAP client creates message "foo" from address "primary" of "userMoreAddresses" to "john.doe@email.com" with body "hello world" in "Sent"
|
||||
Then IMAP response is "OK"
|
||||
When the event loop of "userMoreAddresses" loops once
|
||||
Then mailbox "Sent" for "userMoreAddresses" has messages
|
||||
| from | to | subject | read |
|
||||
| [primary] | john.doe@email.com | foo | true |
|
||||
And mailbox "INBOX" for "userMoreAddresses" has no messages
|
||||
|
||||
@ignore
|
||||
Scenario: Creates message sent from user's secondary address
|
||||
Given there is IMAP client selected in "Sent"
|
||||
When IMAP client creates message "foo" from address "secondary" of "userMoreAddresses" to "john.doe@email.com" with body "hello world" in "Sent"
|
||||
Then IMAP response is "OK"
|
||||
When the event loop of "userMoreAddresses" loops once
|
||||
Then mailbox "Sent" for "userMoreAddresses" has messages
|
||||
| from | to | subject | read |
|
||||
| [secondary] | john.doe@email.com | foo | true |
|
||||
And mailbox "INBOX" for "userMoreAddresses" has no messages
|
||||
|
||||
Scenario: Imports an unrelated message to inbox
|
||||
Given there is IMAP client selected in "INBOX"
|
||||
When IMAP client creates message "foo" from "john.doe@email.com" to "john.doe2@email.com" with body "hello world" in "INBOX"
|
||||
Then IMAP response is "OK"
|
||||
And mailbox "INBOX" for "userMoreAddresses" has messages
|
||||
| from | to | subject | read |
|
||||
| john.doe@email.com | john.doe2@email.com | foo | true |
|
||||
|
||||
Scenario: Imports an unrelated message to sent
|
||||
Given there is IMAP client selected in "Sent"
|
||||
When IMAP client creates message "foo" from "notuser@gmail.com" to "alsonotuser@gmail.com" with body "hello world" in "Sent"
|
||||
Then IMAP response is "OK"
|
||||
When the event loop of "userMoreAddresses" loops once
|
||||
Then mailbox "Sent" for "userMoreAddresses" has messages
|
||||
| from | to | subject | read |
|
||||
| notuser@gmail.com | alsonotuser@gmail.com | foo | true |
|
||||
And mailbox "INBOX" for "userMoreAddresses" has no messages
|
||||
41
test/features/imap/message/delete.feature
Normal file
41
test/features/imap/message/delete.feature
Normal file
@ -0,0 +1,41 @@
|
||||
Feature: IMAP delete messages
|
||||
Background:
|
||||
Given there is connected user "user"
|
||||
And there is "user" with mailbox "Folders/mbox"
|
||||
And there is "user" with mailbox "Labels/label"
|
||||
|
||||
# https://gitlab.protontech.ch/ProtonMail/Slim-API/issues/1420
|
||||
@ignore-live
|
||||
Scenario Outline: Delete message
|
||||
Given there are 10 messages in mailbox "<mailbox>" for "user"
|
||||
And there is IMAP client logged in as "user"
|
||||
And there is IMAP client selected in "<mailbox>"
|
||||
When IMAP client deletes messages "1"
|
||||
Then IMAP response is "OK"
|
||||
And mailbox "<mailbox>" for "user" has 9 messages
|
||||
|
||||
Examples:
|
||||
| mailbox |
|
||||
| INBOX |
|
||||
| Folders/mbox |
|
||||
| Labels/label |
|
||||
| Drafts |
|
||||
| Trash |
|
||||
|
||||
# https://gitlab.protontech.ch/ProtonMail/Slim-API/issues/1420
|
||||
@ignore-live
|
||||
Scenario Outline: Delete all messages
|
||||
Given there are 10 messages in mailbox "<mailbox>" for "user"
|
||||
And there is IMAP client logged in as "user"
|
||||
And there is IMAP client selected in "<mailbox>"
|
||||
When IMAP client deletes messages "1:*"
|
||||
Then IMAP response is "OK"
|
||||
And mailbox "<mailbox>" for "user" has 0 messages
|
||||
|
||||
Examples:
|
||||
| mailbox |
|
||||
| INBOX |
|
||||
| Folders/mbox |
|
||||
| Labels/label |
|
||||
| Drafts |
|
||||
| Trash |
|
||||
51
test/features/imap/message/fetch.feature
Normal file
51
test/features/imap/message/fetch.feature
Normal file
@ -0,0 +1,51 @@
|
||||
Feature: IMAP fetch messages
|
||||
Background:
|
||||
Given there is connected user "user"
|
||||
And there is "user" with mailbox "Folders/mbox"
|
||||
|
||||
Scenario: Fetch of inbox
|
||||
Given there are 10 messages in mailbox "INBOX" for "user"
|
||||
And there is IMAP client logged in as "user"
|
||||
And there is IMAP client selected in "INBOX"
|
||||
When IMAP client fetches "1:*"
|
||||
Then IMAP response is "OK"
|
||||
And IMAP response has 10 messages
|
||||
|
||||
Scenario: Fetch first few message of inbox
|
||||
Given there are 10 messages in mailbox "INBOX" for "user"
|
||||
And there is IMAP client logged in as "user"
|
||||
And there is IMAP client selected in "INBOX"
|
||||
When IMAP client fetches "1:5"
|
||||
Then IMAP response is "OK"
|
||||
And IMAP response has 5 messages
|
||||
|
||||
Scenario: Fetch of custom mailbox
|
||||
Given there are 10 messages in mailbox "Folders/mbox" for "user"
|
||||
And there is IMAP client logged in as "user"
|
||||
And there is IMAP client selected in "Folders/mbox"
|
||||
When IMAP client fetches "1:*"
|
||||
Then IMAP response is "OK"
|
||||
And IMAP response has 10 messages
|
||||
|
||||
Scenario: Fetch of emtpy mailbox
|
||||
Given there is IMAP client logged in as "user"
|
||||
And there is IMAP client selected in "Folders/mbox"
|
||||
When IMAP client fetches "1:*"
|
||||
Then IMAP response is "OK"
|
||||
And IMAP response has 0 messages
|
||||
|
||||
Scenario: Fetch of big mailbox
|
||||
Given there are 100 messages in mailbox "Folders/mbox" for "user"
|
||||
And there is IMAP client logged in as "user"
|
||||
And there is IMAP client selected in "Folders/mbox"
|
||||
When IMAP client fetches "1:*"
|
||||
Then IMAP response is "OK"
|
||||
And IMAP response has 100 messages
|
||||
|
||||
Scenario: Fetch returns alsways latest messages
|
||||
Given there are 10 messages in mailbox "Folders/mbox" for "user"
|
||||
And there is IMAP client logged in as "user"
|
||||
And there is IMAP client selected in "Folders/mbox"
|
||||
When IMAP client fetches by UID "11:*"
|
||||
Then IMAP response is "OK"
|
||||
And IMAP response has 1 message
|
||||
54
test/features/imap/message/move.feature
Normal file
54
test/features/imap/message/move.feature
Normal file
@ -0,0 +1,54 @@
|
||||
Feature: IMAP move messages
|
||||
Background:
|
||||
Given there is connected user "user"
|
||||
And there is "user" with mailbox "Folders/mbox"
|
||||
And there are messages in mailbox "INBOX" for "user"
|
||||
| from | to | subject | body |
|
||||
| john.doe@mail.com | user@pm.me | foo | hello |
|
||||
| jane.doe@mail.com | name@pm.me | bar | world |
|
||||
And there is IMAP client logged in as "user"
|
||||
And there is IMAP client selected in "INBOX"
|
||||
|
||||
@ignore
|
||||
Scenario: Move message
|
||||
When IMAP client moves messages "1" to "Folders/mbox"
|
||||
Then IMAP response is "OK"
|
||||
And mailbox "INBOX" for "user" has messages
|
||||
| from | to | subject |
|
||||
| jane.doe@mail.com | name@pm.me | bar |
|
||||
And mailbox "Folders/mbox" for "user" has messages
|
||||
| from | to | subject |
|
||||
| john.doe@mail.com | user@pm.me | foo |
|
||||
|
||||
@ignore
|
||||
Scenario: Move all messages
|
||||
When IMAP client moves messages "1:*" to "Folders/mbox"
|
||||
Then IMAP response is "OK"
|
||||
And mailbox "INBOX" for "user" has 0 messages
|
||||
And mailbox "Folders/mbox" for "user" has messages
|
||||
| from | to | subject |
|
||||
| john.doe@mail.com | user@pm.me | foo |
|
||||
| jane.doe@mail.com | name@pm.me | bar |
|
||||
|
||||
@ignore
|
||||
Scenario: Move message to All Mail
|
||||
When IMAP client moves messages "1" to "All Mail"
|
||||
Then IMAP response is "OK"
|
||||
And mailbox "INBOX" for "user" has messages
|
||||
| from | to | subject |
|
||||
| jane.doe@mail.com | name@pm.me | bar |
|
||||
And mailbox "All Mail" for "user" has messages
|
||||
| from | to | subject |
|
||||
| john.doe@mail.com | user@pm.me | foo |
|
||||
|
||||
@ignore
|
||||
Scenario: Move message from All Mail is not possible
|
||||
When IMAP client moves messages "1" to "Folders/mbox"
|
||||
Then IMAP response is "OK"
|
||||
And mailbox "All Mail" for "user" has messages
|
||||
| from | to | subject |
|
||||
| john.doe@mail.com | user@pm.me | foo |
|
||||
| jane.doe@mail.com | name@pm.me | bar |
|
||||
And mailbox "Folders/mbox" for "user" has messages
|
||||
| from | to | subject |
|
||||
| john.doe@mail.com | user@pm.me | foo |
|
||||
29
test/features/imap/message/search.feature
Normal file
29
test/features/imap/message/search.feature
Normal file
@ -0,0 +1,29 @@
|
||||
Feature: IMAP search messages
|
||||
Background:
|
||||
Given there is connected user "user"
|
||||
Given there are messages in mailbox "INBOX" for "user"
|
||||
| from | to | subject | body |
|
||||
| john.doe@mail.com | user@pm.me | foo | hello |
|
||||
| jane.doe@mail.com | name@pm.me | bar | world |
|
||||
And there is IMAP client logged in as "user"
|
||||
And there is IMAP client selected in "INBOX"
|
||||
|
||||
Scenario: Search by subject
|
||||
When IMAP client searches for "SUBJECT foo"
|
||||
Then IMAP response is "OK"
|
||||
And IMAP response has 1 message
|
||||
|
||||
Scenario: Search by text
|
||||
When IMAP client searches for "TEXT world"
|
||||
Then IMAP response is "OK"
|
||||
And IMAP response has 1 message
|
||||
|
||||
Scenario: Search by from
|
||||
When IMAP client searches for "FROM jane.doe@email.com"
|
||||
Then IMAP response is "OK"
|
||||
And IMAP response has 1 message
|
||||
|
||||
Scenario: Search by to
|
||||
When IMAP client searches for "TO user@pm.me"
|
||||
Then IMAP response is "OK"
|
||||
And IMAP response has 1 message
|
||||
35
test/features/imap/message/update.feature
Normal file
35
test/features/imap/message/update.feature
Normal file
@ -0,0 +1,35 @@
|
||||
Feature: IMAP update messages
|
||||
Background:
|
||||
Given there is connected user "user"
|
||||
And there are messages in mailbox "INBOX" for "user"
|
||||
| from | to | subject | body | read | starred |
|
||||
| john.doe@mail.com | user@pm.me | foo | hello | false | false |
|
||||
| jane.doe@mail.com | name@pm.me | bar | world | true | true |
|
||||
And there is IMAP client logged in as "user"
|
||||
And there is IMAP client selected in "INBOX"
|
||||
|
||||
Scenario: Mark message as read
|
||||
When IMAP client marks message "2" as read
|
||||
Then IMAP response is "OK"
|
||||
And message "1" in "INBOX" for "user" is marked as read
|
||||
And message "1" in "INBOX" for "user" is marked as unstarred
|
||||
|
||||
Scenario: Mark message as unread
|
||||
When IMAP client marks message "1" as unread
|
||||
Then IMAP response is "OK"
|
||||
And message "2" in "INBOX" for "user" is marked as unread
|
||||
And message "2" in "INBOX" for "user" is marked as starred
|
||||
|
||||
Scenario: Mark message as starred
|
||||
Then message "1" in "INBOX" for "user" is marked as unread
|
||||
And message "1" in "INBOX" for "user" is marked as unstarred
|
||||
When IMAP client marks message "2" as starred
|
||||
Then IMAP response is "OK"
|
||||
And message "1" in "INBOX" for "user" is marked as unread
|
||||
And message "1" in "INBOX" for "user" is marked as starred
|
||||
|
||||
Scenario: Mark message as unstarred
|
||||
When IMAP client marks message "1" as unstarred
|
||||
Then IMAP response is "OK"
|
||||
And message "2" in "INBOX" for "user" is marked as read
|
||||
And message "2" in "INBOX" for "user" is marked as unstarred
|
||||
65
test/features/smtp/auth.feature
Normal file
65
test/features/smtp/auth.feature
Normal file
@ -0,0 +1,65 @@
|
||||
Feature: SMTP auth
|
||||
Scenario: Ask EHLO
|
||||
Given there is connected user "user"
|
||||
When SMTP client sends EHLO
|
||||
Then SMTP response is "OK"
|
||||
|
||||
Scenario: Authenticates successfully and EHLO successfully
|
||||
Given there is connected user "user"
|
||||
When SMTP client authenticates "user"
|
||||
Then SMTP response is "OK"
|
||||
When SMTP client sends EHLO
|
||||
Then SMTP response is "OK"
|
||||
|
||||
Scenario: Authenticates with bad password
|
||||
Given there is connected user "user"
|
||||
When SMTP client authenticates "user" with bad password
|
||||
Then SMTP response is "SMTP error: 454 backend/credentials: incorrect password"
|
||||
|
||||
Scenario: Authenticates with disconnected user
|
||||
Given there is disconnected user "user"
|
||||
When SMTP client authenticates "user"
|
||||
Then SMTP response is "SMTP error: 454 bridge account is logged out, use bridge to login again"
|
||||
|
||||
Scenario: Authenticates with no user
|
||||
When SMTP client authenticates with username "user@pm.me" and password "bridgepassword"
|
||||
Then SMTP response is "SMTP error: 454 user user@pm.me not found"
|
||||
|
||||
Scenario: Authenticates with capital letter
|
||||
Given there is connected user "userAddressWithCapitalLetter"
|
||||
When SMTP client authenticates "userAddressWithCapitalLetter"
|
||||
Then SMTP response is "OK"
|
||||
|
||||
Scenario: Authenticates with more addresses - primary one
|
||||
Given there is connected user "userMoreAddresses"
|
||||
When SMTP client authenticates "userMoreAddresses" with address "primary"
|
||||
Then SMTP response is "OK"
|
||||
|
||||
Scenario: Authenticates with more addresses - secondary one
|
||||
Given there is connected user "userMoreAddresses"
|
||||
When SMTP client authenticates "userMoreAddresses" with address "secondary"
|
||||
Then SMTP response is "OK"
|
||||
|
||||
Scenario: Authenticates with more addresses - disabled address
|
||||
Given there is connected user "userMoreAddresses"
|
||||
When SMTP client authenticates "userMoreAddresses" with address "disabled"
|
||||
Then SMTP response is "SMTP error: 454 user .* not found"
|
||||
|
||||
@ignore-live
|
||||
Scenario: Authenticates with disabled primary address
|
||||
Given there is connected user "userDisabledPrimaryAddress"
|
||||
When SMTP client authenticates "userDisabledPrimaryAddress" with address "primary"
|
||||
Then SMTP response is "OK"
|
||||
|
||||
Scenario: Authenticates two users
|
||||
Given there is connected user "user"
|
||||
And there is connected user "userMoreAddresses"
|
||||
When SMTP client "smtp1" authenticates "user"
|
||||
Then SMTP response to "smtp1" is "OK"
|
||||
When SMTP client "smtp2" authenticates "userMoreAddresses" with address "primary"
|
||||
Then SMTP response to "smtp2" is "OK"
|
||||
|
||||
Scenario: Logs out user
|
||||
Given there is connected user "user"
|
||||
When SMTP client logs out
|
||||
Then SMTP response is "OK"
|
||||
69
test/features/smtp/send/bcc.feature
Normal file
69
test/features/smtp/send/bcc.feature
Normal file
@ -0,0 +1,69 @@
|
||||
Feature: SMTP with bcc
|
||||
Background:
|
||||
Given there is connected user "user"
|
||||
And there is SMTP client logged in as "user"
|
||||
|
||||
Scenario: Send message to address in to and bcc
|
||||
When SMTP client sends message with bcc "bridgetest2@protonmail.com"
|
||||
"""
|
||||
Subject: hello
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
To: Internal Bridge <bridgetest@protonmail.com>
|
||||
|
||||
hello
|
||||
|
||||
"""
|
||||
Then SMTP response is "OK"
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| time | from | to | subject |
|
||||
| now | [userAddress] | bridgetest@protonmail.com | hello |
|
||||
And message is sent with API call:
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
"Subject": "hello",
|
||||
"ToList": [
|
||||
{
|
||||
"Address": "bridgetest@protonmail.com",
|
||||
"Name": "Internal Bridge"
|
||||
}
|
||||
],
|
||||
"CCList": [],
|
||||
"BCCList": [
|
||||
{
|
||||
"Address": "bridgetest2@protonmail.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
Scenario: Send message only to bcc
|
||||
When SMTP client sends message with bcc "bridgetest@protonmail.com"
|
||||
"""
|
||||
Subject: hello
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
|
||||
hello
|
||||
|
||||
"""
|
||||
Then SMTP response is "OK"
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| time | from | to | subject |
|
||||
| now | [userAddress] | | hello |
|
||||
And message is sent with API call:
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
"Subject": "hello",
|
||||
"ToList": [],
|
||||
"CCList": [],
|
||||
"BCCList": [
|
||||
{
|
||||
"Address": "bridgetest@protonmail.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"""
|
||||
56
test/features/smtp/send/failures.feature
Normal file
56
test/features/smtp/send/failures.feature
Normal file
@ -0,0 +1,56 @@
|
||||
Feature: SMTP wrong messages
|
||||
Background:
|
||||
Given there is connected user "user"
|
||||
And there is SMTP client logged in as "user"
|
||||
|
||||
Scenario: Message with no charset and bad character
|
||||
When SMTP client sends message
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
To: External Bridge <pm.bridge.qa@gmail.com>
|
||||
Subject: Plain text, no charset, wrong base64 external
|
||||
Content-Disposition: inline
|
||||
Content-Type: text/plain;
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
sdfsdfsd
|
||||
|
||||
"""
|
||||
Then SMTP response is "SMTP error: 554 Error: transaction failed, blame it on the weather: non-utf8 content without charset specification"
|
||||
|
||||
Scenario: Message with attachment and wrong boundaries
|
||||
When SMTP client sends message
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
To: Internal Bridge <bridgetest@protonmail.com>
|
||||
Subject: With attachment (wrong boundaries)
|
||||
Content-Type: multipart/related; boundary=bc5bd30245232f31b6c976adcd59bb0069c9b13f986f9e40c2571bb80aa16606
|
||||
|
||||
--bc5bd30245232f31b6c976adcd59bb0069c9b13f986f9e40c2571bb80aa16606
|
||||
Content-Disposition: inline
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
This is body of mail with attachment
|
||||
|
||||
--bc5bd30245232f31b6c976adcd59bb0069c9b13f986f9e40c2571bb80aa16606
|
||||
Content-Disposition: attachment; filename=outline-light-instagram-48.png
|
||||
Content-Id: <9114fe6f0adfaf7fdf7a@protonmail.com>
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Type: image/png
|
||||
|
||||
iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAALVBMVEUAAAD/////////////////
|
||||
//////////////////////////////////////+hSKubAAAADnRSTlMAgO8QQM+/IJ9gj1AwcIQd
|
||||
OXUAAAGdSURBVDjLXJC9SgNBFIVPXDURTYhgIQghINgowyLYCAYtRFAIgtYhpAjYhC0srCRW6YIg
|
||||
WNpoHVSsg/gEii+Qnfxq4DyDc3cyMfrBwl2+O+fOHTi8p7LS5RUf/9gpMKL7iT9sK47Q95ggpkzv
|
||||
1cvRcsGYNMYsmP+zKN27NR2vcDyTNVdfkOuuniNPMWafvIbljt+YoMEvW8y7lt+ARwhvrgPjhA0I
|
||||
BTng7S1GLPlypBvtIBPidY4YBDJFdtnkscQ5JGaGqxC9i7jSDwcwnB8qHWBaQjw1ABI8wYgtVoG6
|
||||
9pFkH8iZIiJeulFt4JLvJq8I5N2GMWYbHWDWzM3JZTMdeSWla0kW86FcuI0mfStiNKQ/AhEeh8h0
|
||||
YUTffFwrMTT5oSwdojIQ0UKcocgAKRH1HiqhFQmmJa5qRaYHNbRiSsOgslY0NdixItUTUWlZkedP
|
||||
HXVyAgAIA1F0wP5btQZPIyTwvAqa/Fl4oacuP+e4XHAjSYpkQkxSiMX+T7FPoZJToSStzED70HCy
|
||||
KE3NGCg4jJrC6Ti7AFwZLhnW0gMbzFZc0RmmeAAAAABJRU5ErkJggg==
|
||||
--bc5bd30245232f31b6c976adcd59bb0069c9b13f986f9e40c2571bb80aa16606
|
||||
|
||||
|
||||
"""
|
||||
Then SMTP response is "SMTP error: 554 Error: transaction failed, blame it on the weather: multipart: NextPart: EOF"
|
||||
297
test/features/smtp/send/html.feature
Normal file
297
test/features/smtp/send/html.feature
Normal file
@ -0,0 +1,297 @@
|
||||
Feature: SMTP sending of HTML messages
|
||||
Background:
|
||||
Given there is connected user "user"
|
||||
And there is SMTP client logged in as "user"
|
||||
|
||||
Scenario: HTML message to external account
|
||||
When SMTP client sends message
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
To: External Bridge <pm.bridge.qa@gmail.com>
|
||||
Subject: HTML text external
|
||||
Content-Disposition: inline
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Type: text/html; charset=utf-8
|
||||
In-Reply-To: base64hashOfSomeMessage@protonmail.internalID
|
||||
References: base64hashOfSomeConversation@protonmail.internalID base64hashOfSomeConversation@protonmail.conversationID
|
||||
|
||||
<html><body>This is body of <b>HTML mail</b> without attachment<body></html>
|
||||
|
||||
"""
|
||||
Then SMTP response is "OK"
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| time | from | to | subject |
|
||||
| now | [userAddress] | pm.bridge.qa@gmail.com | HTML text external |
|
||||
And message is sent with API call:
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
"Subject": "HTML text external",
|
||||
"Sender": {
|
||||
"Name": "Bridge Test"
|
||||
},
|
||||
"ToList": [
|
||||
{
|
||||
"Address": "pm.bridge.qa@gmail.com",
|
||||
"Name": "External Bridge"
|
||||
}
|
||||
],
|
||||
"CCList": [],
|
||||
"BCCList": [],
|
||||
"MIMEType": "text/html"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: HTML message with inline image to external account
|
||||
When SMTP client sends message
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
To: External Bridge <pm.bridge.qa@gmail.com>
|
||||
Subject: Html Inline External
|
||||
Content-Disposition: inline
|
||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Thunderbird/60.5.0
|
||||
MIME-Version: 1.0
|
||||
Content-Language: en-US
|
||||
Content-Type: multipart/related; boundary="------------61FA22A41A3F46E8E90EF528"
|
||||
|
||||
This is a multi-part message in MIME format.
|
||||
--------------61FA22A41A3F46E8E90EF528
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
</head>
|
||||
<body text="#000000" bgcolor="#FFFFFF">
|
||||
<p><br>
|
||||
</p>
|
||||
<p>Behold! An inline <img moz-do-not-send="false"
|
||||
src="cid:part1.D96BFAE9.E2E1CAE3@protonmail.com" alt=""
|
||||
width="24" height="24"><br>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
--------------61FA22A41A3F46E8E90EF528
|
||||
Content-Type: image/gif; name="email-action-left.gif"
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-ID: <part1.D96BFAE9.E2E1CAE3@protonmail.com>
|
||||
Content-Disposition: inline; filename="email-action-left.gif"
|
||||
|
||||
R0lGODlhGAAYANUAACcsKOHs4kppTH6tgYWxiIq0jTVENpG5lDI/M7bRuEaJSkqOTk2RUU+P
|
||||
U16lYl+lY2iva262cXS6d3rDfYLNhWeeamKTZGSVZkNbRGqhbOPt4////+7u7qioqFZWVlNT
|
||||
UyIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAGAAYAAAG
|
||||
/8CNcLjRJAqVRqNSSGiI0GFgoKhar4NAdHioMhyRCYUyiTgY1cOWUH1ILgIDAGAQXCSPKgHa
|
||||
XUAyGCCCg4IYGRALCmpCAVUQFgiEkiAIFhBVWhtUDxmRk5IIGXkDRQoMEoGfHpIYEmhGCg4X
|
||||
nyAdHB+SFw4KRwoRArQdG7eEAhEKSAoTBoIdzs/Cw7iCBhMKSQoUAIJbQ8QgABQKStnbIN1C
|
||||
3+HjFcrMtdDO6dMg1dcFvsCfwt+CxsgJYs3a10+QLl4aTKGitYpQq1eaFHDyREtQqFGMHEGq
|
||||
SMkSJi4K/ACiZQiRIihsJL6JM6fOnTwK9kTpYgqMGDJm0JzsNuWKTw0FWdANMYJECRMnW4IA
|
||||
ADs=
|
||||
--------------61FA22A41A3F46E8E90EF528--
|
||||
|
||||
"""
|
||||
Then SMTP response is "OK"
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| time | from | to | subject |
|
||||
| now | [userAddress] | pm.bridge.qa@gmail.com | Html Inline External |
|
||||
And message is sent with API call:
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
"Subject": "Html Inline External",
|
||||
"Sender": {
|
||||
"Name": "Bridge Test"
|
||||
},
|
||||
"ToList": [
|
||||
{
|
||||
"Address": "pm.bridge.qa@gmail.com",
|
||||
"Name": "External Bridge"
|
||||
}
|
||||
],
|
||||
"CCList": [],
|
||||
"BCCList": [],
|
||||
"MIMEType": "text/html"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: HTML message with alternative inline to internal account
|
||||
When SMTP client sends message
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
To: Internal Bridge <bridgetest@protonmail.com>
|
||||
Subject: Html Inline Alternative Internal
|
||||
Content-Disposition: inline
|
||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Thunderbird/60.5.0
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/alternative; boundary="------------5A259F4DE164B5ADA313F644"
|
||||
Content-Language: en-US
|
||||
|
||||
This is a multi-part message in MIME format.
|
||||
--------------5A259F4DE164B5ADA313F644
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
|
||||
Behold! An inline
|
||||
|
||||
|
||||
--------------5A259F4DE164B5ADA313F644
|
||||
Content-Type: multipart/related; boundary="------------61FA22A41A3F46E8E90EF528"
|
||||
|
||||
|
||||
--------------61FA22A41A3F46E8E90EF528
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
</head>
|
||||
<body text="#000000" bgcolor="#FFFFFF">
|
||||
<p><br>
|
||||
</p>
|
||||
<p>Behold! An inline <img moz-do-not-send="false"
|
||||
src="cid:part1.D96BFAE9.E2E1CAE3@protonmail.com" alt=""
|
||||
width="24" height="24"><br>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
--------------61FA22A41A3F46E8E90EF528
|
||||
Content-Type: image/gif; name="email-action-left.gif"
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-ID: <part1.D96BFAE9.E2E1CAE3@protonmail.com>
|
||||
Content-Disposition: inline; filename="email-action-left.gif"
|
||||
|
||||
R0lGODlhGAAYANUAACcsKOHs4kppTH6tgYWxiIq0jTVENpG5lDI/M7bRuEaJSkqOTk2RUU+P
|
||||
U16lYl+lY2iva262cXS6d3rDfYLNhWeeamKTZGSVZkNbRGqhbOPt4////+7u7qioqFZWVlNT
|
||||
UyIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAGAAYAAAG
|
||||
/8CNcLjRJAqVRqNSSGiI0GFgoKhar4NAdHioMhyRCYUyiTgY1cOWUH1ILgIDAGAQXCSPKgHa
|
||||
XUAyGCCCg4IYGRALCmpCAVUQFgiEkiAIFhBVWhtUDxmRk5IIGXkDRQoMEoGfHpIYEmhGCg4X
|
||||
nyAdHB+SFw4KRwoRArQdG7eEAhEKSAoTBoIdzs/Cw7iCBhMKSQoUAIJbQ8QgABQKStnbIN1C
|
||||
3+HjFcrMtdDO6dMg1dcFvsCfwt+CxsgJYs3a10+QLl4aTKGitYpQq1eaFHDyREtQqFGMHEGq
|
||||
SMkSJi4K/ACiZQiRIihsJL6JM6fOnTwK9kTpYgqMGDJm0JzsNuWKTw0FWdANMYJECRMnW4IA
|
||||
ADs=
|
||||
--------------61FA22A41A3F46E8E90EF528--
|
||||
|
||||
--------------5A259F4DE164B5ADA313F644--
|
||||
|
||||
"""
|
||||
Then SMTP response is "OK"
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| time | from | to | subject |
|
||||
| now | [userAddress] | bridgetest@protonmail.com | Html Inline Alternative Internal |
|
||||
And message is sent with API call:
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
"Subject": "Html Inline Alternative Internal",
|
||||
"Sender": {
|
||||
"Name": "Bridge Test"
|
||||
},
|
||||
"ToList": [
|
||||
{
|
||||
"Address": "bridgetest@protonmail.com",
|
||||
"Name": "Internal Bridge"
|
||||
}
|
||||
],
|
||||
"CCList": [],
|
||||
"BCCList": [],
|
||||
"MIMEType": "text/html"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: HTML message with alternative inline to external account
|
||||
When SMTP client sends message
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
To: External Bridge <pm.bridge.qa@gmail.com>
|
||||
Subject: Html Inline Alternative External
|
||||
Content-Disposition: inline
|
||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Thunderbird/60.5.0
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/alternative; boundary="------------5A259F4DE164B5ADA313F644"
|
||||
Content-Language: en-US
|
||||
|
||||
This is a multi-part message in MIME format.
|
||||
--------------5A259F4DE164B5ADA313F644
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
|
||||
Behold! An inline
|
||||
|
||||
|
||||
--------------5A259F4DE164B5ADA313F644
|
||||
Content-Type: multipart/related; boundary="------------61FA22A41A3F46E8E90EF528"
|
||||
|
||||
|
||||
--------------61FA22A41A3F46E8E90EF528
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
</head>
|
||||
<body text="#000000" bgcolor="#FFFFFF">
|
||||
<p><br>
|
||||
</p>
|
||||
<p>Behold! An inline <img moz-do-not-send="false"
|
||||
src="cid:part1.D96BFAE9.E2E1CAE3@protonmail.com" alt=""
|
||||
width="24" height="24"><br>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
--------------61FA22A41A3F46E8E90EF528
|
||||
Content-Type: image/gif; name="email-action-left.gif"
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-ID: <part1.D96BFAE9.E2E1CAE3@protonmail.com>
|
||||
Content-Disposition: inline; filename="email-action-left.gif"
|
||||
|
||||
R0lGODlhGAAYANUAACcsKOHs4kppTH6tgYWxiIq0jTVENpG5lDI/M7bRuEaJSkqOTk2RUU+P
|
||||
U16lYl+lY2iva262cXS6d3rDfYLNhWeeamKTZGSVZkNbRGqhbOPt4////+7u7qioqFZWVlNT
|
||||
UyIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAGAAYAAAG
|
||||
/8CNcLjRJAqVRqNSSGiI0GFgoKhar4NAdHioMhyRCYUyiTgY1cOWUH1ILgIDAGAQXCSPKgHa
|
||||
XUAyGCCCg4IYGRALCmpCAVUQFgiEkiAIFhBVWhtUDxmRk5IIGXkDRQoMEoGfHpIYEmhGCg4X
|
||||
nyAdHB+SFw4KRwoRArQdG7eEAhEKSAoTBoIdzs/Cw7iCBhMKSQoUAIJbQ8QgABQKStnbIN1C
|
||||
3+HjFcrMtdDO6dMg1dcFvsCfwt+CxsgJYs3a10+QLl4aTKGitYpQq1eaFHDyREtQqFGMHEGq
|
||||
SMkSJi4K/ACiZQiRIihsJL6JM6fOnTwK9kTpYgqMGDJm0JzsNuWKTw0FWdANMYJECRMnW4IA
|
||||
ADs=
|
||||
--------------61FA22A41A3F46E8E90EF528--
|
||||
|
||||
--------------5A259F4DE164B5ADA313F644--
|
||||
|
||||
"""
|
||||
Then SMTP response is "OK"
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| time | from | to | subject |
|
||||
| now | [userAddress] | pm.bridge.qa@gmail.com | Html Inline Alternative External |
|
||||
And message is sent with API call:
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
"Subject": "Html Inline Alternative External",
|
||||
"Sender": {
|
||||
"Name": "Bridge Test"
|
||||
},
|
||||
"ToList": [
|
||||
{
|
||||
"Address": "pm.bridge.qa@gmail.com",
|
||||
"Name": "External Bridge"
|
||||
}
|
||||
],
|
||||
"CCList": [],
|
||||
"BCCList": [],
|
||||
"MIMEType": "text/html"
|
||||
}
|
||||
}
|
||||
"""
|
||||
122
test/features/smtp/send/html_att.feature
Normal file
122
test/features/smtp/send/html_att.feature
Normal file
@ -0,0 +1,122 @@
|
||||
Feature: SMTP sending of HTML messages with attachments
|
||||
Background:
|
||||
Given there is connected user "user"
|
||||
And there is SMTP client logged in as "user"
|
||||
|
||||
Scenario: HTML message with attachment to internal account
|
||||
When SMTP client sends message
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
To: Internal Bridge <bridgetest@protonmail.com>
|
||||
Subject: HTML with attachment internal
|
||||
Content-Type: multipart/related; boundary=bc5bd30245232f31b6c976adcd59bb0069c9b13f986f9e40c2571bb80aa16606
|
||||
|
||||
--bc5bd30245232f31b6c976adcd59bb0069c9b13f986f9e40c2571bb80aa16606
|
||||
Content-Disposition: inline
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Type: text/html; charset=utf-8
|
||||
|
||||
<html><body>This is body of <b>HTML mail</b> with attachment<body></html>
|
||||
|
||||
--bc5bd30245232f31b6c976adcd59bb0069c9b13f986f9e40c2571bb80aa16606
|
||||
Content-Disposition: attachment; filename=outline-light-instagram-48.png
|
||||
Content-Id: <9114fe6f0adfaf7fdf7a@protonmail.com>
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Type: image/png
|
||||
|
||||
iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAALVBMVEUAAAD/////////////////
|
||||
//////////////////////////////////////+hSKubAAAADnRSTlMAgO8QQM+/IJ9gj1AwcIQd
|
||||
OXUAAAGdSURBVDjLXJC9SgNBFIVPXDURTYhgIQghINgowyLYCAYtRFAIgtYhpAjYhC0srCRW6YIg
|
||||
WNpoHVSsg/gEii+Qnfxq4DyDc3cyMfrBwl2+O+fOHTi8p7LS5RUf/9gpMKL7iT9sK47Q95ggpkzv
|
||||
1cvRcsGYNMYsmP+zKN27NR2vcDyTNVdfkOuuniNPMWafvIbljt+YoMEvW8y7lt+ARwhvrgPjhA0I
|
||||
BTng7S1GLPlypBvtIBPidY4YBDJFdtnkscQ5JGaGqxC9i7jSDwcwnB8qHWBaQjw1ABI8wYgtVoG6
|
||||
9pFkH8iZIiJeulFt4JLvJq8I5N2GMWYbHWDWzM3JZTMdeSWla0kW86FcuI0mfStiNKQ/AhEeh8h0
|
||||
YUTffFwrMTT5oSwdojIQ0UKcocgAKRH1HiqhFQmmJa5qRaYHNbRiSsOgslY0NdixItUTUWlZkedP
|
||||
HXVyAgAIA1F0wP5btQZPIyTwvAqa/Fl4oacuP+e4XHAjSYpkQkxSiMX+T7FPoZJToSStzED70HCy
|
||||
KE3NGCg4jJrC6Ti7AFwZLhnW0gMbzFZc0RmmeAAAAABJRU5ErkJggg==
|
||||
--bc5bd30245232f31b6c976adcd59bb0069c9b13f986f9e40c2571bb80aa16606--
|
||||
|
||||
"""
|
||||
Then SMTP response is "OK"
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| time | from | to | subject |
|
||||
| now | [userAddress] | bridgetest@protonmail.com | HTML with attachment internal |
|
||||
And message is sent with API call:
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
"Subject": "HTML with attachment internal",
|
||||
"Sender": {
|
||||
"Name": "Bridge Test"
|
||||
},
|
||||
"ToList": [
|
||||
{
|
||||
"Address": "bridgetest@protonmail.com",
|
||||
"Name": "Internal Bridge"
|
||||
}
|
||||
],
|
||||
"CCList": [],
|
||||
"BCCList": [],
|
||||
"MIMEType": "text/html"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: HTML message with attachment to external account
|
||||
When SMTP client sends message
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
To: External Bridge <pm.bridge.qa@gmail.com>
|
||||
Subject: HTML with attachment external PGP
|
||||
Content-Type: multipart/mixed; boundary=bc5bd30245232f31b6c976adcd59bb0069c9b13f986f9e40c2571bb80aa16606
|
||||
|
||||
--bc5bd30245232f31b6c976adcd59bb0069c9b13f986f9e40c2571bb80aa16606
|
||||
Content-Disposition: inline
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Type: text/html; charset=utf-8
|
||||
|
||||
<html><body>This is body of <b>HTML mail</b> with attachment<body></html>
|
||||
|
||||
--bc5bd30245232f31b6c976adcd59bb0069c9b13f986f9e40c2571bb80aa16606
|
||||
Content-Disposition: attachment; filename=outline-light-instagram-48.png
|
||||
Content-Id: <9114fe6f0adfaf7fdf7a@protonmail.com>
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Type: image/png
|
||||
|
||||
iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAALVBMVEUAAAD/////////////////
|
||||
//////////////////////////////////////+hSKubAAAADnRSTlMAgO8QQM+/IJ9gj1AwcIQd
|
||||
OXUAAAGdSURBVDjLXJC9SgNBFIVPXDURTYhgIQghINgowyLYCAYtRFAIgtYhpAjYhC0srCRW6YIg
|
||||
WNpoHVSsg/gEii+Qnfxq4DyDc3cyMfrBwl2+O+fOHTi8p7LS5RUf/9gpMKL7iT9sK47Q95ggpkzv
|
||||
1cvRcsGYNMYsmP+zKN27NR2vcDyTNVdfkOuuniNPMWafvIbljt+YoMEvW8y7lt+ARwhvrgPjhA0I
|
||||
BTng7S1GLPlypBvtIBPidY4YBDJFdtnkscQ5JGaGqxC9i7jSDwcwnB8qHWBaQjw1ABI8wYgtVoG6
|
||||
9pFkH8iZIiJeulFt4JLvJq8I5N2GMWYbHWDWzM3JZTMdeSWla0kW86FcuI0mfStiNKQ/AhEeh8h0
|
||||
YUTffFwrMTT5oSwdojIQ0UKcocgAKRH1HiqhFQmmJa5qRaYHNbRiSsOgslY0NdixItUTUWlZkedP
|
||||
HXVyAgAIA1F0wP5btQZPIyTwvAqa/Fl4oacuP+e4XHAjSYpkQkxSiMX+T7FPoZJToSStzED70HCy
|
||||
KE3NGCg4jJrC6Ti7AFwZLhnW0gMbzFZc0RmmeAAAAABJRU5ErkJggg==
|
||||
--bc5bd30245232f31b6c976adcd59bb0069c9b13f986f9e40c2571bb80aa16606--
|
||||
|
||||
"""
|
||||
Then SMTP response is "OK"
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| time | from | to | subject |
|
||||
| now | [userAddress] | pm.bridge.qa@gmail.com | HTML with attachment external PGP |
|
||||
And message is sent with API call:
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
"Subject": "HTML with attachment external PGP",
|
||||
"Sender": {
|
||||
"Name": "Bridge Test"
|
||||
},
|
||||
"ToList": [
|
||||
{
|
||||
"Address": "pm.bridge.qa@gmail.com",
|
||||
"Name": "External Bridge"
|
||||
}
|
||||
],
|
||||
"CCList": [],
|
||||
"BCCList": [],
|
||||
"MIMEType": "text/html"
|
||||
}
|
||||
}
|
||||
"""
|
||||
218
test/features/smtp/send/plain.feature
Normal file
218
test/features/smtp/send/plain.feature
Normal file
@ -0,0 +1,218 @@
|
||||
Feature: SMTP sending of plain messages
|
||||
Background:
|
||||
Given there is connected user "user"
|
||||
And there is SMTP client logged in as "user"
|
||||
|
||||
Scenario: Only from and to headers to internal account
|
||||
When SMTP client sends message
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
To: Internal Bridge <bridgetest@protonmail.com>
|
||||
|
||||
hello
|
||||
|
||||
"""
|
||||
Then SMTP response is "OK"
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| time | from | to | subject |
|
||||
| now | [userAddress] | bridgetest@protonmail.com | |
|
||||
And message is sent with API call:
|
||||
"""
|
||||
{
|
||||
"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 sends message
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
To: External Bridge <pm.bridge.qa@gmail.com>
|
||||
|
||||
hello
|
||||
|
||||
"""
|
||||
Then SMTP response is "OK"
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| time | from | to | subject |
|
||||
| now | [userAddress] | pm.bridge.qa@gmail.com | |
|
||||
And message is sent with API call:
|
||||
"""
|
||||
{
|
||||
"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 sends message
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
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 SMTP response is "OK"
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| time | from | to | subject |
|
||||
| now | [userAddress] | bridgetest@protonmail.com | Plain text internal |
|
||||
And message is sent with API call:
|
||||
"""
|
||||
{
|
||||
"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 sends message
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
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 SMTP response is "OK"
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| time | from | to | subject |
|
||||
| now | [userAddress] | pm.bridge.qa@gmail.com | Plain text external |
|
||||
And message is sent with API call:
|
||||
"""
|
||||
{
|
||||
"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
|
||||
When SMTP client sends message
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
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 SMTP response is "OK"
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| time | from | to | subject |
|
||||
| now | [userAddress] | pm.bridge.qa@gmail.com | Plain text no charset external |
|
||||
And message is sent with API call:
|
||||
"""
|
||||
{
|
||||
"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 sends message
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
To: External Bridge <pm.bridge.qa@gmail.com>
|
||||
Subject: Plain, no charset, no content, external
|
||||
Content-Disposition: inline
|
||||
Content-Type: text/plain;
|
||||
|
||||
"""
|
||||
Then SMTP response is "OK"
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| time | from | to | subject |
|
||||
| now | [userAddress] | pm.bridge.qa@gmail.com | Plain, no charset, no content, external |
|
||||
And message is sent with API call:
|
||||
"""
|
||||
{
|
||||
"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/html"
|
||||
}
|
||||
}
|
||||
"""
|
||||
187
test/features/smtp/send/plain_att.feature
Normal file
187
test/features/smtp/send/plain_att.feature
Normal file
@ -0,0 +1,187 @@
|
||||
Feature: SMTP sending of plain messages with attachments
|
||||
Background:
|
||||
Given there is connected user "user"
|
||||
And there is SMTP client logged in as "user"
|
||||
|
||||
Scenario: Basic message with attachment to internal account
|
||||
When SMTP client sends message
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
To: Internal Bridge <bridgetest@protonmail.com>
|
||||
Subject: Plain with attachment
|
||||
Content-Type: multipart/related; boundary=bc5bd30245232f31b6c976adcd59bb0069c9b13f986f9e40c2571bb80aa16606
|
||||
|
||||
--bc5bd30245232f31b6c976adcd59bb0069c9b13f986f9e40c2571bb80aa16606
|
||||
Content-Disposition: inline
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
This is body of mail with attachment
|
||||
|
||||
--bc5bd30245232f31b6c976adcd59bb0069c9b13f986f9e40c2571bb80aa16606
|
||||
Content-Disposition: attachment; filename=outline-light-instagram-48.png
|
||||
Content-Id: <9114fe6f0adfaf7fdf7a@protonmail.com>
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Type: image/png
|
||||
|
||||
iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAALVBMVEUAAAD/////////////////
|
||||
//////////////////////////////////////+hSKubAAAADnRSTlMAgO8QQM+/IJ9gj1AwcIQd
|
||||
OXUAAAGdSURBVDjLXJC9SgNBFIVPXDURTYhgIQghINgowyLYCAYtRFAIgtYhpAjYhC0srCRW6YIg
|
||||
WNpoHVSsg/gEii+Qnfxq4DyDc3cyMfrBwl2+O+fOHTi8p7LS5RUf/9gpMKL7iT9sK47Q95ggpkzv
|
||||
1cvRcsGYNMYsmP+zKN27NR2vcDyTNVdfkOuuniNPMWafvIbljt+YoMEvW8y7lt+ARwhvrgPjhA0I
|
||||
BTng7S1GLPlypBvtIBPidY4YBDJFdtnkscQ5JGaGqxC9i7jSDwcwnB8qHWBaQjw1ABI8wYgtVoG6
|
||||
9pFkH8iZIiJeulFt4JLvJq8I5N2GMWYbHWDWzM3JZTMdeSWla0kW86FcuI0mfStiNKQ/AhEeh8h0
|
||||
YUTffFwrMTT5oSwdojIQ0UKcocgAKRH1HiqhFQmmJa5qRaYHNbRiSsOgslY0NdixItUTUWlZkedP
|
||||
HXVyAgAIA1F0wP5btQZPIyTwvAqa/Fl4oacuP+e4XHAjSYpkQkxSiMX+T7FPoZJToSStzED70HCy
|
||||
KE3NGCg4jJrC6Ti7AFwZLhnW0gMbzFZc0RmmeAAAAABJRU5ErkJggg==
|
||||
--bc5bd30245232f31b6c976adcd59bb0069c9b13f986f9e40c2571bb80aa16606--
|
||||
|
||||
"""
|
||||
Then SMTP response is "OK"
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| time | from | to | subject |
|
||||
| now | [userAddress] | bridgetest@protonmail.com | Plain with attachment |
|
||||
And message is sent with API call:
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
"Subject": "Plain with attachment",
|
||||
"Sender": {
|
||||
"Name": "Bridge Test"
|
||||
},
|
||||
"ToList": [
|
||||
{
|
||||
"Address": "bridgetest@protonmail.com",
|
||||
"Name": "Internal Bridge"
|
||||
}
|
||||
],
|
||||
"CCList": [],
|
||||
"BCCList": [],
|
||||
"MIMEType": "text/plain"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: Plain message with attachment to external account
|
||||
When SMTP client sends message
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
To: External Bridge <pm.bridge.qa@gmail.com>
|
||||
Subject: Plain with attachment external
|
||||
Content-Type: multipart/related; boundary=bc5bd30245232f31b6c976adcd59bb0069c9b13f986f9e40c2571bb80aa16606
|
||||
|
||||
--bc5bd30245232f31b6c976adcd59bb0069c9b13f986f9e40c2571bb80aa16606
|
||||
Content-Disposition: inline
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
This is body of mail with attachment
|
||||
|
||||
--bc5bd30245232f31b6c976adcd59bb0069c9b13f986f9e40c2571bb80aa16606
|
||||
Content-Disposition: attachment; filename=outline-light-instagram-48.png
|
||||
Content-Id: <9114fe6f0adfaf7fdf7a@protonmail.com>
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Type: image/png
|
||||
|
||||
iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAALVBMVEUAAAD/////////////////
|
||||
//////////////////////////////////////+hSKubAAAADnRSTlMAgO8QQM+/IJ9gj1AwcIQd
|
||||
OXUAAAGdSURBVDjLXJC9SgNBFIVPXDURTYhgIQghINgowyLYCAYtRFAIgtYhpAjYhC0srCRW6YIg
|
||||
WNpoHVSsg/gEii+Qnfxq4DyDc3cyMfrBwl2+O+fOHTi8p7LS5RUf/9gpMKL7iT9sK47Q95ggpkzv
|
||||
1cvRcsGYNMYsmP+zKN27NR2vcDyTNVdfkOuuniNPMWafvIbljt+YoMEvW8y7lt+ARwhvrgPjhA0I
|
||||
BTng7S1GLPlypBvtIBPidY4YBDJFdtnkscQ5JGaGqxC9i7jSDwcwnB8qHWBaQjw1ABI8wYgtVoG6
|
||||
9pFkH8iZIiJeulFt4JLvJq8I5N2GMWYbHWDWzM3JZTMdeSWla0kW86FcuI0mfStiNKQ/AhEeh8h0
|
||||
YUTffFwrMTT5oSwdojIQ0UKcocgAKRH1HiqhFQmmJa5qRaYHNbRiSsOgslY0NdixItUTUWlZkedP
|
||||
HXVyAgAIA1F0wP5btQZPIyTwvAqa/Fl4oacuP+e4XHAjSYpkQkxSiMX+T7FPoZJToSStzED70HCy
|
||||
KE3NGCg4jJrC6Ti7AFwZLhnW0gMbzFZc0RmmeAAAAABJRU5ErkJggg==
|
||||
--bc5bd30245232f31b6c976adcd59bb0069c9b13f986f9e40c2571bb80aa16606--
|
||||
|
||||
"""
|
||||
Then SMTP response is "OK"
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| time | from | to | subject |
|
||||
| now | [userAddress] | pm.bridge.qa@gmail.com | Plain with attachment external |
|
||||
And message is sent with API call:
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
"Subject": "Plain with attachment external",
|
||||
"Sender": {
|
||||
"Name": "Bridge Test"
|
||||
},
|
||||
"ToList": [
|
||||
{
|
||||
"Address": "pm.bridge.qa@gmail.com",
|
||||
"Name": "External Bridge"
|
||||
}
|
||||
],
|
||||
"CCList": [],
|
||||
"BCCList": [],
|
||||
"MIMEType": "text/plain"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: Plain message with attachment to two external accounts
|
||||
When SMTP client sends message
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
To: External Bridge 1 <pm.bridge.qa@gmail.com>
|
||||
CC: External Bridge 2 <bridgeqa@seznam.cz>
|
||||
Subject: Plain with attachment external PGP and external CC
|
||||
Content-Type: multipart/mixed; boundary=bc5bd30245232f31b6c976adcd59bb0069c9b13f986f9e40c2571bb80aa16606
|
||||
|
||||
--bc5bd30245232f31b6c976adcd59bb0069c9b13f986f9e40c2571bb80aa16606
|
||||
Content-Disposition: inline
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
This is body of mail with attachment
|
||||
|
||||
--bc5bd30245232f31b6c976adcd59bb0069c9b13f986f9e40c2571bb80aa16606
|
||||
Content-Disposition: attachment; filename=outline-light-instagram-48.png
|
||||
Content-Id: <9114fe6f0adfaf7fdf7a@protonmail.com>
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Type: image/png
|
||||
|
||||
iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAALVBMVEUAAAD/////////////////
|
||||
//////////////////////////////////////+hSKubAAAADnRSTlMAgO8QQM+/IJ9gj1AwcIQd
|
||||
OXUAAAGdSURBVDjLXJC9SgNBFIVPXDURTYhgIQghINgowyLYCAYtRFAIgtYhpAjYhC0srCRW6YIg
|
||||
WNpoHVSsg/gEii+Qnfxq4DyDc3cyMfrBwl2+O+fOHTi8p7LS5RUf/9gpMKL7iT9sK47Q95ggpkzv
|
||||
1cvRcsGYNMYsmP+zKN27NR2vcDyTNVdfkOuuniNPMWafvIbljt+YoMEvW8y7lt+ARwhvrgPjhA0I
|
||||
BTng7S1GLPlypBvtIBPidY4YBDJFdtnkscQ5JGaGqxC9i7jSDwcwnB8qHWBaQjw1ABI8wYgtVoG6
|
||||
9pFkH8iZIiJeulFt4JLvJq8I5N2GMWYbHWDWzM3JZTMdeSWla0kW86FcuI0mfStiNKQ/AhEeh8h0
|
||||
YUTffFwrMTT5oSwdojIQ0UKcocgAKRH1HiqhFQmmJa5qRaYHNbRiSsOgslY0NdixItUTUWlZkedP
|
||||
HXVyAgAIA1F0wP5btQZPIyTwvAqa/Fl4oacuP+e4XHAjSYpkQkxSiMX+T7FPoZJToSStzED70HCy
|
||||
KE3NGCg4jJrC6Ti7AFwZLhnW0gMbzFZc0RmmeAAAAABJRU5ErkJggg==
|
||||
--bc5bd30245232f31b6c976adcd59bb0069c9b13f986f9e40c2571bb80aa16606--
|
||||
|
||||
"""
|
||||
Then SMTP response is "OK"
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| time | from | to | cc | subject |
|
||||
| now | [userAddress] | pm.bridge.qa@gmail.com | bridgeqa@seznam.cz | Plain with attachment external PGP and external CC |
|
||||
And message is sent with API call:
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
"Subject": "Plain with attachment external PGP and external CC",
|
||||
"Sender": {
|
||||
"Name": "Bridge Test"
|
||||
},
|
||||
"ToList": [
|
||||
{
|
||||
"Address": "pm.bridge.qa@gmail.com",
|
||||
"Name": "External Bridge 1"
|
||||
}
|
||||
],
|
||||
"CCList": [
|
||||
{
|
||||
"Address": "bridgeqa@seznam.cz",
|
||||
"Name": "External Bridge 2"
|
||||
}
|
||||
],
|
||||
"BCCList": [],
|
||||
"MIMEType": "text/plain"
|
||||
}
|
||||
}
|
||||
"""
|
||||
45
test/features/smtp/send/same_message.feature
Normal file
45
test/features/smtp/send/same_message.feature
Normal file
@ -0,0 +1,45 @@
|
||||
Feature: SMTP sending the same message twice
|
||||
Background:
|
||||
Given there is connected user "user"
|
||||
And there is SMTP client logged in as "user"
|
||||
When SMTP client sends message
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
To: Internal Bridge <bridgetest@protonmail.com>
|
||||
Subject: Hello
|
||||
|
||||
World
|
||||
|
||||
"""
|
||||
Then SMTP response is "OK"
|
||||
|
||||
Scenario: The exact same message is not sent twice
|
||||
When SMTP client sends message
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
To: Internal Bridge <bridgetest@protonmail.com>
|
||||
Subject: Hello
|
||||
|
||||
World
|
||||
|
||||
"""
|
||||
Then SMTP response is "OK"
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| time | from | to | subject |
|
||||
| now | [userAddress] | bridgetest@protonmail.com | Hello |
|
||||
|
||||
Scenario: Slight change means different message and is sent twice
|
||||
When SMTP client sends message
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
To: Internal Bridge <bridgetest@protonmail.com>
|
||||
Subject: Hello.
|
||||
|
||||
World
|
||||
|
||||
"""
|
||||
Then SMTP response is "OK"
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| time | from | to | subject |
|
||||
| now | [userAddress] | bridgetest@protonmail.com | Hello |
|
||||
| now | [userAddress] | bridgetest@protonmail.com | Hello. |
|
||||
71
test/features/smtp/send/two_messages.feature
Normal file
71
test/features/smtp/send/two_messages.feature
Normal file
@ -0,0 +1,71 @@
|
||||
Feature: SMTP sending two messages
|
||||
Scenario: Send two messages in one connection
|
||||
Given there is connected user "user"
|
||||
And there is SMTP client logged in as "user"
|
||||
When SMTP client sends message
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
To: Internal Bridge <bridgetest@protonmail.com>
|
||||
|
||||
hello
|
||||
|
||||
"""
|
||||
Then SMTP response is "OK"
|
||||
When SMTP client sends message
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
To: Internal Bridge <bridgetest@protonmail.com>
|
||||
|
||||
world
|
||||
|
||||
"""
|
||||
Then SMTP response is "OK"
|
||||
|
||||
Scenario: Send to two addresses
|
||||
Given there is connected user "userMoreAddresses"
|
||||
And there is "userMoreAddresses" in "split" address mode
|
||||
And there is SMTP client "smtp1" logged in as "userMoreAddresses" with address "primary"
|
||||
And there is SMTP client "smtp2" logged in as "userMoreAddresses" with address "secondary"
|
||||
When SMTP client "smtp1" sends message
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
To: Internal Bridge <bridgetest@protonmail.com>
|
||||
|
||||
hello
|
||||
|
||||
"""
|
||||
Then SMTP response to "smtp1" is "OK"
|
||||
When SMTP client "smtp2" sends message
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
To: Internal Bridge <bridgetest@protonmail.com>
|
||||
|
||||
world
|
||||
|
||||
"""
|
||||
Then SMTP response to "smtp2" is "OK"
|
||||
|
||||
Scenario: Send to two users
|
||||
Given there is connected user "user"
|
||||
And there is connected user "userMoreAddresses"
|
||||
And there is SMTP client "smtp1" logged in as "user"
|
||||
And there is SMTP client "smtp2" logged in as "userMoreAddresses"
|
||||
|
||||
When SMTP client "smtp1" sends message
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
To: Internal Bridge <bridgetest@protonmail.com>
|
||||
|
||||
hello
|
||||
|
||||
"""
|
||||
Then SMTP response to "smtp1" is "OK"
|
||||
When SMTP client "smtp2" sends message
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
To: Internal Bridge <bridgetest@protonmail.com>
|
||||
|
||||
world
|
||||
|
||||
"""
|
||||
Then SMTP response to "smtp2" is "OK"
|
||||
82
test/imap_actions_auth_test.go
Normal file
82
test/imap_actions_auth_test.go
Normal file
@ -0,0 +1,82 @@
|
||||
// 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 tests
|
||||
|
||||
import (
|
||||
"github.com/cucumber/godog"
|
||||
)
|
||||
|
||||
func IMAPActionsAuthFeatureContext(s *godog.Suite) {
|
||||
s.Step(`^IMAP client authenticates "([^"]*)"$`, imapClientAuthenticates)
|
||||
s.Step(`^IMAP client "([^"]*)" authenticates "([^"]*)"$`, imapClientNamedAuthenticates)
|
||||
s.Step(`^IMAP client authenticates "([^"]*)" with address "([^"]*)"$`, imapClientAuthenticatesWithAddress)
|
||||
s.Step(`^IMAP client "([^"]*)" authenticates "([^"]*)" with address "([^"]*)"$`, imapClientNamedAuthenticatesWithAddress)
|
||||
s.Step(`^IMAP client authenticates "([^"]*)" with bad password$`, imapClientAuthenticatesWithBadPassword)
|
||||
s.Step(`^IMAP client authenticates with username "([^"]*)" and password "([^"]*)"$`, imapClientAuthenticatesWithUsernameAndPassword)
|
||||
s.Step(`^IMAP client logs out$`, imapClientLogsOut)
|
||||
}
|
||||
|
||||
func imapClientAuthenticates(bddUserID string) error {
|
||||
return imapClientNamedAuthenticates("imap", bddUserID)
|
||||
}
|
||||
|
||||
func imapClientNamedAuthenticates(clientID, bddUserID string) error {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
res := ctx.GetIMAPClient(clientID).Login(account.Address(), account.BridgePassword())
|
||||
ctx.SetIMAPLastResponse(clientID, res)
|
||||
return nil
|
||||
}
|
||||
|
||||
func imapClientAuthenticatesWithAddress(bddUserID, bddAddressID string) error {
|
||||
return imapClientNamedAuthenticatesWithAddress("imap", bddUserID, bddAddressID)
|
||||
}
|
||||
|
||||
func imapClientNamedAuthenticatesWithAddress(clientID, bddUserID, bddAddressID string) error {
|
||||
account := ctx.GetTestAccountWithAddress(bddUserID, bddAddressID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
res := ctx.GetIMAPClient(clientID).Login(account.Address(), account.BridgePassword())
|
||||
ctx.SetIMAPLastResponse(clientID, res)
|
||||
return nil
|
||||
}
|
||||
|
||||
func imapClientAuthenticatesWithBadPassword(bddUserID string) error {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
res := ctx.GetIMAPClient("imap").Login(account.Address(), "you shall not pass!")
|
||||
ctx.SetIMAPLastResponse("imap", res)
|
||||
return nil
|
||||
}
|
||||
|
||||
func imapClientAuthenticatesWithUsernameAndPassword(username, password string) error {
|
||||
res := ctx.GetIMAPClient("imap").Login(username, password)
|
||||
ctx.SetIMAPLastResponse("imap", res)
|
||||
return nil
|
||||
}
|
||||
|
||||
func imapClientLogsOut() error {
|
||||
res := ctx.GetIMAPClient("imap").Logout()
|
||||
ctx.SetIMAPLastResponse("imap", res)
|
||||
return nil
|
||||
}
|
||||
74
test/imap_actions_mailbox_test.go
Normal file
74
test/imap_actions_mailbox_test.go
Normal file
@ -0,0 +1,74 @@
|
||||
// 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 tests
|
||||
|
||||
import (
|
||||
"github.com/cucumber/godog"
|
||||
)
|
||||
|
||||
func IMAPActionsMailboxFeatureContext(s *godog.Suite) {
|
||||
s.Step(`^IMAP client creates mailbox "([^"]*)"$`, imapClientCreatesMailbox)
|
||||
s.Step(`^IMAP client renames mailbox "([^"]*)" to "([^"]*)"$`, imapClientRenamesMailboxTo)
|
||||
s.Step(`^IMAP client deletes mailbox "([^"]*)"$`, imapClientDeletesMailbox)
|
||||
s.Step(`^IMAP client lists mailboxes$`, imapClientListsMailboxes)
|
||||
s.Step(`^IMAP client selects "([^"]*)"$`, imapClientSelects)
|
||||
s.Step(`^IMAP client gets info of "([^"]*)"$`, imapClientGetsInfoOf)
|
||||
s.Step(`^IMAP client gets status of "([^"]*)"$`, imapClientGetsStatusOf)
|
||||
}
|
||||
|
||||
func imapClientCreatesMailbox(mailboxName string) error {
|
||||
res := ctx.GetIMAPClient("imap").CreateMailbox(mailboxName)
|
||||
ctx.SetIMAPLastResponse("imap", res)
|
||||
return nil
|
||||
}
|
||||
|
||||
func imapClientRenamesMailboxTo(mailboxName, newMailboxName string) error {
|
||||
res := ctx.GetIMAPClient("imap").RenameMailbox(mailboxName, newMailboxName)
|
||||
ctx.SetIMAPLastResponse("imap", res)
|
||||
return nil
|
||||
}
|
||||
|
||||
func imapClientDeletesMailbox(mailboxName string) error {
|
||||
res := ctx.GetIMAPClient("imap").DeleteMailbox(mailboxName)
|
||||
ctx.SetIMAPLastResponse("imap", res)
|
||||
return nil
|
||||
}
|
||||
|
||||
func imapClientListsMailboxes() error {
|
||||
res := ctx.GetIMAPClient("imap").ListMailboxes()
|
||||
ctx.SetIMAPLastResponse("imap", res)
|
||||
return nil
|
||||
}
|
||||
|
||||
func imapClientSelects(mailboxName string) error {
|
||||
res := ctx.GetIMAPClient("imap").Select(mailboxName)
|
||||
ctx.SetIMAPLastResponse("imap", res)
|
||||
return nil
|
||||
}
|
||||
|
||||
func imapClientGetsInfoOf(mailboxName string) error {
|
||||
res := ctx.GetIMAPClient("imap").GetMailboxInfo(mailboxName)
|
||||
ctx.SetIMAPLastResponse("imap", res)
|
||||
return nil
|
||||
}
|
||||
|
||||
func imapClientGetsStatusOf(mailboxName string) error {
|
||||
res := ctx.GetIMAPClient("imap").GetMailboxStatus(mailboxName)
|
||||
ctx.SetIMAPLastResponse("imap", res)
|
||||
return nil
|
||||
}
|
||||
158
test/imap_actions_messages_test.go
Normal file
158
test/imap_actions_messages_test.go
Normal file
@ -0,0 +1,158 @@
|
||||
// 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 tests
|
||||
|
||||
import (
|
||||
"github.com/cucumber/godog"
|
||||
)
|
||||
|
||||
func IMAPActionsMessagesFeatureContext(s *godog.Suite) {
|
||||
s.Step(`^IMAP client fetches "([^"]*)"$`, imapClientFetches)
|
||||
s.Step(`^IMAP client fetches by UID "([^"]*)"$`, imapClientFetchesByUID)
|
||||
s.Step(`^IMAP client searches for "([^"]*)"$`, imapClientSearchesFor)
|
||||
s.Step(`^IMAP client deletes messages "([^"]*)"$`, imapClientDeletesMessages)
|
||||
s.Step(`^IMAP client "([^"]*)" deletes messages "([^"]*)"$`, imapClientNamedDeletesMessages)
|
||||
s.Step(`^IMAP client copies messages "([^"]*)" to "([^"]*)"$`, imapClientCopiesMessagesTo)
|
||||
s.Step(`^IMAP client moves messages "([^"]*)" to "([^"]*)"$`, imapClientMovesMessagesTo)
|
||||
s.Step(`^IMAP client creates message "([^"]*)" from "([^"]*)" to "([^"]*)" with body "([^"]*)" in "([^"]*)"$`, imapClientCreatesMessageFromToWithBody)
|
||||
s.Step(`^IMAP client creates message "([^"]*)" from "([^"]*)" to address "([^"]*)" of "([^"]*)" with body "([^"]*)" in "([^"]*)"$`, imapClientCreatesMessageFromToAddressOfUserWithBody)
|
||||
s.Step(`^IMAP client creates message "([^"]*)" from address "([^"]*)" of "([^"]*)" to "([^"]*)" with body "([^"]*)" in "([^"]*)"$`, imapClientCreatesMessageFromAddressOfUserToWithBody)
|
||||
s.Step(`^IMAP client marks message "([^"]*)" as read$`, imapClientMarksMessageAsRead)
|
||||
s.Step(`^IMAP client "([^"]*)" marks message "([^"]*)" as read$`, imapClientNamedMarksMessageAsRead)
|
||||
s.Step(`^IMAP client marks message "([^"]*)" as unread$`, imapClientMarksMessageAsUnread)
|
||||
s.Step(`^IMAP client "([^"]*)" marks message "([^"]*)" as unread$`, imapClientNamedMarksMessageAsUnread)
|
||||
s.Step(`^IMAP client marks message "([^"]*)" as starred$`, imapClientMarksMessageAsStarred)
|
||||
s.Step(`^IMAP client "([^"]*)" marks message "([^"]*)" as starred$`, imapClientNamedMarksMessageAsStarred)
|
||||
s.Step(`^IMAP client marks message "([^"]*)" as unstarred$`, imapClientMarksMessageAsUnstarred)
|
||||
s.Step(`^IMAP client "([^"]*)" marks message "([^"]*)" as unstarred$`, imapClientNamedMarksMessageAsUnstarred)
|
||||
s.Step(`^IMAP client starts IDLE-ing$`, imapClientStartsIDLEing)
|
||||
s.Step(`^IMAP client "([^"]*)" starts IDLE-ing$`, imapClientNamedStartsIDLEing)
|
||||
}
|
||||
|
||||
func imapClientFetches(fetchRange string) error {
|
||||
res := ctx.GetIMAPClient("imap").Fetch(fetchRange, "UID")
|
||||
ctx.SetIMAPLastResponse("imap", res)
|
||||
return nil
|
||||
}
|
||||
|
||||
func imapClientFetchesByUID(fetchRange string) error {
|
||||
res := ctx.GetIMAPClient("imap").FetchUID(fetchRange, "UID")
|
||||
ctx.SetIMAPLastResponse("imap", res)
|
||||
return nil
|
||||
}
|
||||
|
||||
func imapClientSearchesFor(query string) error {
|
||||
res := ctx.GetIMAPClient("imap").Search(query)
|
||||
ctx.SetIMAPLastResponse("imap", res)
|
||||
return nil
|
||||
}
|
||||
|
||||
func imapClientDeletesMessages(messageRange string) error {
|
||||
return imapClientNamedDeletesMessages("imap", messageRange)
|
||||
}
|
||||
|
||||
func imapClientNamedDeletesMessages(imapClient, messageRange string) error {
|
||||
res := ctx.GetIMAPClient(imapClient).Delete(messageRange)
|
||||
ctx.SetIMAPLastResponse(imapClient, res)
|
||||
return nil
|
||||
}
|
||||
|
||||
func imapClientCopiesMessagesTo(messageRange, newMailboxName string) error {
|
||||
res := ctx.GetIMAPClient("imap").Copy(messageRange, newMailboxName)
|
||||
ctx.SetIMAPLastResponse("imap", res)
|
||||
return nil
|
||||
}
|
||||
|
||||
func imapClientMovesMessagesTo(messageRange, newMailboxName string) error {
|
||||
res := ctx.GetIMAPClient("imap").Move(messageRange, newMailboxName)
|
||||
ctx.SetIMAPLastResponse("imap", res)
|
||||
return nil
|
||||
}
|
||||
|
||||
func imapClientCreatesMessageFromToWithBody(subject, from, to, body, mailboxName string) error {
|
||||
res := ctx.GetIMAPClient("imap").Append(mailboxName, subject, from, to, body)
|
||||
ctx.SetIMAPLastResponse("imap", res)
|
||||
return nil
|
||||
}
|
||||
|
||||
func imapClientCreatesMessageFromToAddressOfUserWithBody(subject, from, bddAddressID, bddUserID, body, mailboxName string) error {
|
||||
account := ctx.GetTestAccountWithAddress(bddUserID, bddAddressID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
return imapClientCreatesMessageFromToWithBody(subject, from, account.Address(), body, mailboxName)
|
||||
}
|
||||
|
||||
func imapClientCreatesMessageFromAddressOfUserToWithBody(subject, bddAddressID, bddUserID, to, body, mailboxName string) error {
|
||||
account := ctx.GetTestAccountWithAddress(bddUserID, bddAddressID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
|
||||
return imapClientCreatesMessageFromToWithBody(subject, account.Address(), to, body, mailboxName)
|
||||
}
|
||||
|
||||
func imapClientMarksMessageAsRead(messageRange string) error {
|
||||
return imapClientNamedMarksMessageAsRead("imap", messageRange)
|
||||
}
|
||||
|
||||
func imapClientNamedMarksMessageAsRead(imapClient, messageRange string) error {
|
||||
res := ctx.GetIMAPClient(imapClient).MarkAsRead(messageRange)
|
||||
ctx.SetIMAPLastResponse(imapClient, res)
|
||||
return nil
|
||||
}
|
||||
|
||||
func imapClientMarksMessageAsUnread(messageRange string) error {
|
||||
return imapClientNamedMarksMessageAsUnread("imap", messageRange)
|
||||
}
|
||||
|
||||
func imapClientNamedMarksMessageAsUnread(imapClient, messageRange string) error {
|
||||
res := ctx.GetIMAPClient(imapClient).MarkAsUnread(messageRange)
|
||||
ctx.SetIMAPLastResponse(imapClient, res)
|
||||
return nil
|
||||
}
|
||||
|
||||
func imapClientMarksMessageAsStarred(messageRange string) error {
|
||||
return imapClientNamedMarksMessageAsStarred("imap", messageRange)
|
||||
}
|
||||
|
||||
func imapClientNamedMarksMessageAsStarred(imapClient, messageRange string) error {
|
||||
res := ctx.GetIMAPClient(imapClient).MarkAsStarred(messageRange)
|
||||
ctx.SetIMAPLastResponse(imapClient, res)
|
||||
return nil
|
||||
}
|
||||
|
||||
func imapClientMarksMessageAsUnstarred(messageRange string) error {
|
||||
return imapClientNamedMarksMessageAsUnstarred("imap", messageRange)
|
||||
}
|
||||
|
||||
func imapClientNamedMarksMessageAsUnstarred(imapClient, messageRange string) error {
|
||||
res := ctx.GetIMAPClient(imapClient).MarkAsUnstarred(messageRange)
|
||||
ctx.SetIMAPLastResponse(imapClient, res)
|
||||
return nil
|
||||
}
|
||||
|
||||
func imapClientStartsIDLEing() error {
|
||||
return imapClientNamedStartsIDLEing("imap")
|
||||
}
|
||||
|
||||
func imapClientNamedStartsIDLEing(imapClient string) error {
|
||||
res := ctx.GetIMAPClient(imapClient).StartIDLE()
|
||||
ctx.SetIMAPLastResponse(imapClient, res)
|
||||
return nil
|
||||
}
|
||||
122
test/imap_checks_test.go
Normal file
122
test/imap_checks_test.go
Normal file
@ -0,0 +1,122 @@
|
||||
// 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 tests
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/cucumber/godog"
|
||||
"github.com/emersion/go-imap"
|
||||
)
|
||||
|
||||
func IMAPChecksFeatureContext(s *godog.Suite) {
|
||||
s.Step(`^IMAP response is "([^"]*)"$`, imapResponseIs)
|
||||
s.Step(`^IMAP response to "([^"]*)" is "([^"]*)"$`, imapResponseNamedIs)
|
||||
s.Step(`^IMAP response contains "([^"]*)"$`, imapResponseContains)
|
||||
s.Step(`^IMAP response to "([^"]*)" contains "([^"]*)"$`, imapResponseNamedContains)
|
||||
s.Step(`^IMAP response has (\d+) message(?:s)?$`, imapResponseHasNumberOfMessages)
|
||||
s.Step(`^IMAP response to "([^"]*)" has (\d+) message(?:s)?$`, imapResponseNamedHasNumberOfMessages)
|
||||
s.Step(`^IMAP client receives update marking message "([^"]*)" as read within (\d+) seconds$`, imapClientReceivesUpdateMarkingMessagesAsReadWithin)
|
||||
s.Step(`^IMAP client "([^"]*)" receives update marking message "([^"]*)" as read within (\d+) seconds$`, imapClientNamedReceivesUpdateMarkingMessagesAsReadWithin)
|
||||
s.Step(`^IMAP client receives update marking message "([^"]*)" as unread within (\d+) seconds$`, imapClientReceivesUpdateMarkingMessagesAsUnreadWithin)
|
||||
s.Step(`^IMAP client "([^"]*)" receives update marking message "([^"]*)" as unread within (\d+) seconds$`, imapClientNamedReceivesUpdateMarkingMessagesAsUnreadWithin)
|
||||
s.Step(`^IMAP client "([^"]*)" does not receive update for message "([^"]*)" within (\d+) seconds$`, imapClientDoesNotReceiveUpdateForMessageWithin)
|
||||
}
|
||||
|
||||
func imapResponseIs(expectedResponse string) error {
|
||||
return imapResponseNamedIs("imap", expectedResponse)
|
||||
}
|
||||
|
||||
func imapResponseNamedIs(clientID, expectedResponse string) error {
|
||||
res := ctx.GetIMAPLastResponse(clientID)
|
||||
if expectedResponse == "OK" {
|
||||
res.AssertOK()
|
||||
} else {
|
||||
res.AssertError(expectedResponse)
|
||||
}
|
||||
return ctx.GetTestingError()
|
||||
}
|
||||
|
||||
func imapResponseContains(expectedResponse string) error {
|
||||
return imapResponseNamedContains("imap", expectedResponse)
|
||||
}
|
||||
|
||||
func imapResponseNamedContains(clientID, expectedResponse string) error {
|
||||
res := ctx.GetIMAPLastResponse(clientID)
|
||||
res.AssertSections(expectedResponse)
|
||||
return ctx.GetTestingError()
|
||||
}
|
||||
|
||||
func imapResponseHasNumberOfMessages(expectedCount int) error {
|
||||
return imapResponseNamedHasNumberOfMessages("imap", expectedCount)
|
||||
}
|
||||
|
||||
func imapResponseNamedHasNumberOfMessages(clientID string, expectedCount int) error {
|
||||
res := ctx.GetIMAPLastResponse(clientID)
|
||||
res.AssertSectionsCount(expectedCount)
|
||||
return ctx.GetTestingError()
|
||||
}
|
||||
|
||||
func imapClientReceivesUpdateMarkingMessagesAsReadWithin(messageUIDs string, seconds int) error {
|
||||
return imapClientNamedReceivesUpdateMarkingMessagesAsReadWithin("imap", messageUIDs, seconds)
|
||||
}
|
||||
|
||||
func imapClientNamedReceivesUpdateMarkingMessagesAsReadWithin(clientID, messageUIDs string, seconds int) error {
|
||||
regexps := []string{}
|
||||
iterateOverSeqSet(messageUIDs, func(messageUID string) {
|
||||
regexps = append(regexps, `FETCH \(FLAGS \(.*\\Seen.*\) UID `+messageUID)
|
||||
})
|
||||
ctx.GetIMAPLastResponse(clientID).WaitForSections(time.Duration(seconds)*time.Second, regexps...)
|
||||
return ctx.GetTestingError()
|
||||
}
|
||||
|
||||
func imapClientReceivesUpdateMarkingMessagesAsUnreadWithin(messageUIDs string, seconds int) error {
|
||||
return imapClientNamedReceivesUpdateMarkingMessagesAsUnreadWithin("imap", messageUIDs, seconds)
|
||||
}
|
||||
|
||||
func imapClientNamedReceivesUpdateMarkingMessagesAsUnreadWithin(clientID, messageUIDs string, seconds int) error {
|
||||
regexps := []string{}
|
||||
iterateOverSeqSet(messageUIDs, func(messageUID string) {
|
||||
// Golang does not support negative look ahead. Following complex regexp checks \Seen is not there.
|
||||
regexps = append(regexps, `FETCH \(FLAGS \(([^S]|S[^e]|Se[^e]|See[^n])*\) UID `+messageUID)
|
||||
})
|
||||
ctx.GetIMAPLastResponse(clientID).WaitForSections(time.Duration(seconds)*time.Second, regexps...)
|
||||
return ctx.GetTestingError()
|
||||
}
|
||||
|
||||
func imapClientDoesNotReceiveUpdateForMessageWithin(clientID, messageUIDs string, seconds int) error {
|
||||
regexps := []string{}
|
||||
iterateOverSeqSet(messageUIDs, func(messageUID string) {
|
||||
regexps = append(regexps, `FETCH.*UID `+messageUID)
|
||||
})
|
||||
ctx.GetIMAPLastResponse(clientID).WaitForNotSections(time.Duration(seconds)*time.Second, regexps...)
|
||||
return ctx.GetTestingError()
|
||||
}
|
||||
|
||||
func iterateOverSeqSet(seqSet string, callback func(string)) {
|
||||
seq, err := imap.NewSeqSet(seqSet)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, set := range seq.Set {
|
||||
for i := set.Start; i <= set.Stop; i++ {
|
||||
callback(strconv.Itoa(int(i)))
|
||||
}
|
||||
}
|
||||
}
|
||||
66
test/imap_setup_test.go
Normal file
66
test/imap_setup_test.go
Normal file
@ -0,0 +1,66 @@
|
||||
// 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 tests
|
||||
|
||||
import (
|
||||
"github.com/cucumber/godog"
|
||||
)
|
||||
|
||||
func IMAPSetupFeatureContext(s *godog.Suite) {
|
||||
s.Step(`^there is IMAP client logged in as "([^"]*)"$`, thereIsIMAPClientLoggedInAs)
|
||||
s.Step(`^there is IMAP client "([^"]*)" logged in as "([^"]*)"$`, thereIsIMAPClientNamedLoggedInAs)
|
||||
s.Step(`^there is IMAP client logged in as "([^"]*)" with address "([^"]*)"$`, thereIsIMAPClientLoggedInAsWithAddress)
|
||||
s.Step(`^there is IMAP client "([^"]*)" logged in as "([^"]*)" with address "([^"]*)"$`, thereIsIMAPClientNamedLoggedInAsWithAddress)
|
||||
s.Step(`^there is IMAP client selected in "([^"]*)"$`, thereIsIMAPClientSelectedIn)
|
||||
s.Step(`^there is IMAP client "([^"]*)" selected in "([^"]*)"$`, thereIsIMAPClientNamedSelectedIn)
|
||||
}
|
||||
|
||||
func thereIsIMAPClientLoggedInAs(bddUserID string) error {
|
||||
return thereIsIMAPClientNamedLoggedInAs("imap", bddUserID)
|
||||
}
|
||||
|
||||
func thereIsIMAPClientNamedLoggedInAs(clientID, bddUserID string) (err error) {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
ctx.GetIMAPClient(clientID).Login(account.Address(), account.BridgePassword()).AssertOK()
|
||||
return ctx.GetTestingError()
|
||||
}
|
||||
|
||||
func thereIsIMAPClientLoggedInAsWithAddress(bddUserID, bddAddressID string) error {
|
||||
return thereIsIMAPClientNamedLoggedInAsWithAddress("imap", bddUserID, bddAddressID)
|
||||
}
|
||||
|
||||
func thereIsIMAPClientNamedLoggedInAsWithAddress(clientID, bddUserID, bddAddressID string) error {
|
||||
account := ctx.GetTestAccountWithAddress(bddUserID, bddAddressID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
ctx.GetIMAPClient(clientID).Login(account.Address(), account.BridgePassword()).AssertOK()
|
||||
return nil
|
||||
}
|
||||
|
||||
func thereIsIMAPClientSelectedIn(mailboxName string) error {
|
||||
return thereIsIMAPClientNamedSelectedIn("imap", mailboxName)
|
||||
}
|
||||
|
||||
func thereIsIMAPClientNamedSelectedIn(clientID, mailboxName string) (err error) {
|
||||
ctx.GetIMAPClient(clientID).Select(mailboxName).AssertOK()
|
||||
return ctx.GetTestingError()
|
||||
}
|
||||
28
test/internal_error.go
Normal file
28
test/internal_error.go
Normal file
@ -0,0 +1,28 @@
|
||||
// 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 tests
|
||||
|
||||
import "fmt"
|
||||
|
||||
func internalError(err error, msg string, args ...interface{}) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
msg = fmt.Sprintf(msg, args...)
|
||||
return fmt.Errorf("internal error: %s: %v", msg, err)
|
||||
}
|
||||
75
test/liveapi/calls.go
Normal file
75
test/liveapi/calls.go
Normal file
@ -0,0 +1,75 @@
|
||||
// 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 liveapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/nsf/jsondiff"
|
||||
)
|
||||
|
||||
type fakeCall struct {
|
||||
method string
|
||||
path string
|
||||
request []byte
|
||||
}
|
||||
|
||||
func (cntrl *Controller) recordCall(method, path string, request []byte) {
|
||||
cntrl.lock.Lock()
|
||||
defer cntrl.lock.Unlock()
|
||||
|
||||
cntrl.calls = append(cntrl.calls, &fakeCall{
|
||||
method: method,
|
||||
path: path,
|
||||
request: request,
|
||||
})
|
||||
}
|
||||
|
||||
func (cntrl *Controller) PrintCalls() {
|
||||
fmt.Println("API calls:")
|
||||
for idx, call := range cntrl.calls {
|
||||
fmt.Printf("%02d: [%s] %s\n", idx+1, call.method, call.path)
|
||||
if call.request != nil && string(call.request) != "null" {
|
||||
fmt.Printf("\t%s\n", call.request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cntrl *Controller) WasCalled(method, path string, expectedRequest []byte) bool {
|
||||
for _, call := range cntrl.calls {
|
||||
if call.method != method && call.path != path {
|
||||
continue
|
||||
}
|
||||
diff, _ := jsondiff.Compare(call.request, expectedRequest, &jsondiff.Options{})
|
||||
isSuperset := diff == jsondiff.FullMatch || diff == jsondiff.SupersetMatch
|
||||
if isSuperset {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (cntrl *Controller) GetCalls(method, path string) [][]byte {
|
||||
requests := [][]byte{}
|
||||
for _, call := range cntrl.calls {
|
||||
if call.method == method && call.path == path {
|
||||
requests = append(requests, call.request)
|
||||
}
|
||||
}
|
||||
return requests
|
||||
}
|
||||
132
test/liveapi/cleanup.go
Normal file
132
test/liveapi/cleanup.go
Normal file
@ -0,0 +1,132 @@
|
||||
// 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 liveapi
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func cleanup(client *pmapi.Client) error {
|
||||
if err := cleanSystemFolders(client); err != nil {
|
||||
return errors.Wrap(err, "failed to clean system folders")
|
||||
}
|
||||
if err := cleanCustomLables(client); err != nil {
|
||||
return errors.Wrap(err, "failed to clean cusotm labels")
|
||||
}
|
||||
if err := cleanTrash(client); err != nil {
|
||||
return errors.Wrap(err, "failed to clean trash")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanSystemFolders(client *pmapi.Client) error {
|
||||
for _, labelID := range []string{pmapi.InboxLabel, pmapi.SentLabel, pmapi.ArchiveLabel} {
|
||||
for {
|
||||
messages, total, err := client.ListMessages(&pmapi.MessagesFilter{
|
||||
PageSize: 150,
|
||||
LabelID: labelID,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to list messages")
|
||||
}
|
||||
|
||||
if total == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
messageIDs := []string{}
|
||||
for _, message := range messages {
|
||||
messageIDs = append(messageIDs, message.ID)
|
||||
}
|
||||
|
||||
if err := client.DeleteMessages(messageIDs); err != nil {
|
||||
return errors.Wrap(err, "failed to delete messages")
|
||||
}
|
||||
|
||||
if total == len(messages) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanCustomLables(client *pmapi.Client) error {
|
||||
labels, err := client.ListLabels()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to list labels")
|
||||
}
|
||||
|
||||
for _, label := range labels {
|
||||
if err := emptyFolder(client, label.ID); err != nil {
|
||||
return errors.Wrap(err, "failed to empty label")
|
||||
}
|
||||
if err := client.DeleteLabel(label.ID); err != nil {
|
||||
return errors.Wrap(err, "failed to delete label")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanTrash(client *pmapi.Client) error {
|
||||
for {
|
||||
_, total, err := client.ListMessages(&pmapi.MessagesFilter{
|
||||
PageSize: 1,
|
||||
LabelID: pmapi.TrashLabel,
|
||||
})
|
||||
if err == nil && total == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
err = emptyFolder(client, pmapi.TrashLabel)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if err.Error() == "Folder or label is currently being emptied" {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
return errors.Wrap(err, "failed to empty trash")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func emptyFolder(client *pmapi.Client, labelID string) error {
|
||||
err := client.EmptyFolder(labelID, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for {
|
||||
_, total, err := client.ListMessages(&pmapi.MessagesFilter{
|
||||
PageSize: 1,
|
||||
LabelID: labelID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if total == 0 {
|
||||
break
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
62
test/liveapi/controller.go
Normal file
62
test/liveapi/controller.go
Normal file
@ -0,0 +1,62 @@
|
||||
// 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 liveapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
// Internal states.
|
||||
lock *sync.RWMutex
|
||||
calls []*fakeCall
|
||||
pmapiByUsername map[string]*pmapi.Client
|
||||
messageIDsByUsername map[string][]string
|
||||
|
||||
// State controlled by test.
|
||||
noInternetConnection bool
|
||||
}
|
||||
|
||||
func NewController() *Controller {
|
||||
return &Controller{
|
||||
lock: &sync.RWMutex{},
|
||||
calls: []*fakeCall{},
|
||||
pmapiByUsername: map[string]*pmapi.Client{},
|
||||
messageIDsByUsername: map[string][]string{},
|
||||
|
||||
noInternetConnection: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (cntrl *Controller) GetClient(userID string) *pmapi.Client {
|
||||
cfg := &pmapi.ClientConfig{
|
||||
AppVersion: fmt.Sprintf("Bridge_%s", os.Getenv("VERSION")),
|
||||
ClientID: "bridge-test",
|
||||
Transport: &fakeTransport{
|
||||
cntrl: cntrl,
|
||||
transport: http.DefaultTransport,
|
||||
},
|
||||
TokenManager: pmapi.NewTokenManager(),
|
||||
}
|
||||
return pmapi.NewClient(cfg, userID)
|
||||
}
|
||||
105
test/liveapi/labels.go
Normal file
105
test/liveapi/labels.go
Normal file
@ -0,0 +1,105 @@
|
||||
// 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 liveapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var systemLabelNameToID = map[string]string{ //nolint[gochecknoglobals]
|
||||
"INBOX": pmapi.InboxLabel,
|
||||
"Trash": pmapi.TrashLabel,
|
||||
"Spam": pmapi.SpamLabel,
|
||||
"All Mail": pmapi.AllMailLabel,
|
||||
"Archive": pmapi.ArchiveLabel,
|
||||
"Sent": pmapi.SentLabel,
|
||||
"Drafts": pmapi.DraftLabel,
|
||||
}
|
||||
|
||||
func (cntrl *Controller) AddUserLabel(username string, label *pmapi.Label) error {
|
||||
client, ok := cntrl.pmapiByUsername[username]
|
||||
if !ok {
|
||||
return fmt.Errorf("user %s does not exist", username)
|
||||
}
|
||||
|
||||
label.Exclusive = getLabelExclusive(label.Name)
|
||||
label.Name = getLabelNameWithoutPrefix(label.Name)
|
||||
label.Color = pmapi.LabelColors[0]
|
||||
if _, err := client.CreateLabel(label); err != nil {
|
||||
return errors.Wrap(err, "failed to create label")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cntrl *Controller) GetLabelIDs(username string, labelNames []string) ([]string, error) {
|
||||
labelIDs := []string{}
|
||||
for _, labelName := range labelNames {
|
||||
labelID, err := cntrl.getLabelID(username, labelName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
labelIDs = append(labelIDs, labelID)
|
||||
}
|
||||
return labelIDs, nil
|
||||
}
|
||||
|
||||
func (cntrl *Controller) getLabelID(username, labelName string) (string, error) {
|
||||
if labelID, ok := systemLabelNameToID[labelName]; ok {
|
||||
return labelID, nil
|
||||
}
|
||||
|
||||
client, ok := cntrl.pmapiByUsername[username]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("user %s does not exist", username)
|
||||
}
|
||||
|
||||
labels, err := client.ListLabels()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to list labels")
|
||||
}
|
||||
|
||||
exclusive := getLabelExclusive(labelName)
|
||||
labelName = getLabelNameWithoutPrefix(labelName)
|
||||
for _, label := range labels {
|
||||
if label.Exclusive == exclusive && label.Name == labelName {
|
||||
return label.ID, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("label %s:%s does not exist", username, labelName)
|
||||
}
|
||||
|
||||
func getLabelNameWithoutPrefix(name string) string {
|
||||
if strings.HasPrefix(name, "Folders/") {
|
||||
return strings.TrimPrefix(name, "Folders/")
|
||||
}
|
||||
if strings.HasPrefix(name, "Labels/") {
|
||||
return strings.TrimPrefix(name, "Labels/")
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func getLabelExclusive(name string) int {
|
||||
if strings.HasPrefix(name, "Folders/") {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
134
test/liveapi/messages.go
Normal file
134
test/liveapi/messages.go
Normal file
@ -0,0 +1,134 @@
|
||||
// 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 liveapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
messageUtils "github.com/ProtonMail/proton-bridge/pkg/message"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (cntrl *Controller) AddUserMessage(username string, message *pmapi.Message) error {
|
||||
client, ok := cntrl.pmapiByUsername[username]
|
||||
if !ok {
|
||||
return fmt.Errorf("user %s does not exist", username)
|
||||
}
|
||||
|
||||
body, err := buildMessage(client, message)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to build message")
|
||||
}
|
||||
|
||||
req := &pmapi.ImportMsgReq{
|
||||
AddressID: message.AddressID,
|
||||
Body: body.Bytes(),
|
||||
Unread: message.Unread,
|
||||
Time: message.Time,
|
||||
Flags: message.Flags,
|
||||
LabelIDs: message.LabelIDs,
|
||||
}
|
||||
|
||||
results, err := client.Import([]*pmapi.ImportMsgReq{req})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to make an import")
|
||||
}
|
||||
for _, result := range results {
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(result.Error, "failed to import message")
|
||||
}
|
||||
cntrl.messageIDsByUsername[username] = append(cntrl.messageIDsByUsername[username], result.MessageID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildMessage(client *pmapi.Client, message *pmapi.Message) (*bytes.Buffer, error) {
|
||||
if err := encryptMessage(client, message); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to encrypt message")
|
||||
}
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
if err := buildMessageHeader(message, body); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to build message header")
|
||||
}
|
||||
if err := buildMessageBody(message, body); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to build message body")
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func encryptMessage(client *pmapi.Client, message *pmapi.Message) error {
|
||||
addresses, err := client.GetAddresses()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get address")
|
||||
}
|
||||
kr := addresses.ByID(message.AddressID).KeyRing()
|
||||
|
||||
if err = message.Encrypt(kr, nil); err != nil {
|
||||
return errors.Wrap(err, "failed to encrypt message body")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildMessageHeader(message *pmapi.Message, body *bytes.Buffer) error {
|
||||
header := messageUtils.GetHeader(message)
|
||||
header.Set("Content-Type", "multipart/mixed; boundary="+messageUtils.GetBoundary(message))
|
||||
header.Del("Content-Disposition")
|
||||
header.Del("Content-Transfer-Encoding")
|
||||
|
||||
if err := http.Header(header).Write(body); err != nil {
|
||||
return errors.Wrap(err, "failed to write header")
|
||||
}
|
||||
_, _ = body.WriteString("\r\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildMessageBody(message *pmapi.Message, body *bytes.Buffer) error {
|
||||
mw := multipart.NewWriter(body)
|
||||
if err := mw.SetBoundary(messageUtils.GetBoundary(message)); err != nil {
|
||||
return errors.Wrap(err, "failed to set boundary")
|
||||
}
|
||||
|
||||
bodyHeader := messageUtils.GetBodyHeader(message)
|
||||
bodyHeader.Set("Content-Transfer-Encoding", "7bit")
|
||||
|
||||
part, err := mw.CreatePart(bodyHeader)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create message body part")
|
||||
}
|
||||
if _, err := io.WriteString(part, message.Body); err != nil {
|
||||
return errors.Wrap(err, "failed to write message body")
|
||||
}
|
||||
_ = mw.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cntrl *Controller) GetMessageID(username, messageIndex string) string {
|
||||
idx, err := strconv.Atoi(messageIndex)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("message index %s not found", messageIndex))
|
||||
}
|
||||
return cntrl.messageIDsByUsername[username][idx-1]
|
||||
}
|
||||
59
test/liveapi/transport.go
Normal file
59
test/liveapi/transport.go
Normal file
@ -0,0 +1,59 @@
|
||||
// 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 liveapi
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (cntrl *Controller) TurnInternetConnectionOff() {
|
||||
cntrl.noInternetConnection = true
|
||||
}
|
||||
|
||||
func (cntrl *Controller) TurnInternetConnectionOn() {
|
||||
cntrl.noInternetConnection = false
|
||||
}
|
||||
|
||||
type fakeTransport struct {
|
||||
cntrl *Controller
|
||||
transport http.RoundTripper
|
||||
}
|
||||
|
||||
func (t *fakeTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if t.cntrl.noInternetConnection {
|
||||
return nil, errors.New("no route to host")
|
||||
}
|
||||
|
||||
body := []byte{}
|
||||
if req.GetBody != nil {
|
||||
bodyReader, err := req.GetBody()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get body")
|
||||
}
|
||||
body, err = ioutil.ReadAll(bodyReader)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read body")
|
||||
}
|
||||
}
|
||||
t.cntrl.recordCall(req.Method, req.URL.Path, body)
|
||||
|
||||
return t.transport.RoundTrip(req)
|
||||
}
|
||||
66
test/liveapi/users.go
Normal file
66
test/liveapi/users.go
Normal file
@ -0,0 +1,66 @@
|
||||
// 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 liveapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/cucumber/godog"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (cntrl *Controller) AddUser(user *pmapi.User, addresses *pmapi.AddressList, password string, twoFAEnabled bool) error {
|
||||
if twoFAEnabled {
|
||||
return godog.ErrPending
|
||||
}
|
||||
|
||||
client := pmapi.NewClient(&pmapi.ClientConfig{
|
||||
AppVersion: fmt.Sprintf("Bridge_%s", os.Getenv("VERSION")),
|
||||
ClientID: "bridge-cntrl",
|
||||
TokenManager: pmapi.NewTokenManager(),
|
||||
}, user.ID)
|
||||
|
||||
authInfo, err := client.AuthInfo(user.Name)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get auth info")
|
||||
}
|
||||
auth, err := client.Auth(user.Name, password, authInfo)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to auth user")
|
||||
}
|
||||
|
||||
mailboxPassword, err := pmapi.HashMailboxPassword(password, auth.KeySalt)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to hash mailbox password")
|
||||
}
|
||||
if _, err := client.Unlock(mailboxPassword); err != nil {
|
||||
return errors.Wrap(err, "failed to unlock user")
|
||||
}
|
||||
if err := client.UnlockAddresses([]byte(mailboxPassword)); err != nil {
|
||||
return errors.Wrap(err, "failed to unlock addresses")
|
||||
}
|
||||
|
||||
if err := cleanup(client); err != nil {
|
||||
return errors.Wrap(err, "failed to clean user")
|
||||
}
|
||||
|
||||
cntrl.pmapiByUsername[user.Name] = client
|
||||
return nil
|
||||
}
|
||||
50
test/main_test.go
Normal file
50
test/main_test.go
Normal file
@ -0,0 +1,50 @@
|
||||
// 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 tests
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cucumber/godog"
|
||||
"github.com/cucumber/godog/colors"
|
||||
)
|
||||
|
||||
var opt = godog.Options{ //nolint[gochecknoglobals]
|
||||
Output: colors.Colored(os.Stdout),
|
||||
Format: "progress", // can define default values
|
||||
}
|
||||
|
||||
func init() { //nolint[gochecknoinits]
|
||||
godog.BindFlags("godog.", flag.CommandLine, &opt)
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
flag.Parse()
|
||||
opt.Paths = flag.Args()
|
||||
|
||||
status := godog.RunWithOptions("godogs", func(s *godog.Suite) {
|
||||
FeatureContext(s)
|
||||
}, opt)
|
||||
|
||||
if st := m.Run(); st > status {
|
||||
status = st
|
||||
}
|
||||
os.Exit(status)
|
||||
}
|
||||
78
test/mocks/debug.go
Normal file
78
test/mocks/debug.go
Normal 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
227
test/mocks/imap.go
Normal 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
199
test/mocks/imap_response.go
Normal 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
182
test/mocks/smtp.go
Normal 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...)
|
||||
}
|
||||
42
test/mocks/smtp_response.go
Normal file
42
test/mocks/smtp_response.go
Normal 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
23
test/mocks/testingt.go
Normal 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
26
test/mocks/utils.go
Normal 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))
|
||||
}
|
||||
114
test/smtp_actions_test.go
Normal file
114
test/smtp_actions_test.go
Normal file
@ -0,0 +1,114 @@
|
||||
// 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 tests
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/cucumber/godog"
|
||||
"github.com/cucumber/godog/gherkin"
|
||||
)
|
||||
|
||||
func SMTPActionsAuthFeatureContext(s *godog.Suite) {
|
||||
s.Step(`^SMTP client authenticates "([^"]*)"$`, smtpClientAuthenticates)
|
||||
s.Step(`^SMTP client "([^"]*)" authenticates "([^"]*)"$`, smtpClientNamedAuthenticates)
|
||||
s.Step(`^SMTP client authenticates "([^"]*)" with address "([^"]*)"$`, smtpClientAuthenticatesWithAddress)
|
||||
s.Step(`^SMTP client "([^"]*)" authenticates "([^"]*)" with address "([^"]*)"$`, smtpClientNamedAuthenticatesWithAddress)
|
||||
s.Step(`^SMTP client authenticates "([^"]*)" with bad password$`, smtpClientAuthenticatesWithBadPassword)
|
||||
s.Step(`^SMTP client authenticates with username "([^"]*)" and password "([^"]*)"$`, smtpClientAuthenticatesWithUsernameAndPassword)
|
||||
s.Step(`^SMTP client logs out$`, smtpClientLogsOut)
|
||||
s.Step(`^SMTP client sends message$`, smtpClientSendsMessage)
|
||||
s.Step(`^SMTP client sends EHLO$`, smtpClientSendsEHLO)
|
||||
s.Step(`^SMTP client "([^"]*)" sends message$`, smtpClientNamedSendsMessage)
|
||||
s.Step(`^SMTP client sends message with bcc "([^"]*)"$`, smtpClientSendsMessageWithBCC)
|
||||
s.Step(`^SMTP client "([^"]*)" sends message with bcc "([^"]*)"$`, smtpClientNamedSendsMessageWithBCC)
|
||||
}
|
||||
|
||||
func smtpClientAuthenticates(bddUserID string) error {
|
||||
return smtpClientNamedAuthenticates("smtp", bddUserID)
|
||||
}
|
||||
|
||||
func smtpClientNamedAuthenticates(clientID, bddUserID string) error {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
res := ctx.GetSMTPClient(clientID).Login(account.Address(), account.BridgePassword())
|
||||
ctx.SetSMTPLastResponse(clientID, res)
|
||||
return nil
|
||||
}
|
||||
|
||||
func smtpClientAuthenticatesWithAddress(bddUserID, bddAddressID string) error {
|
||||
return smtpClientNamedAuthenticatesWithAddress("smtp", bddUserID, bddAddressID)
|
||||
}
|
||||
|
||||
func smtpClientNamedAuthenticatesWithAddress(clientID, bddUserID, bddAddressID string) error {
|
||||
account := ctx.GetTestAccountWithAddress(bddUserID, bddAddressID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
res := ctx.GetSMTPClient(clientID).Login(account.Address(), account.BridgePassword())
|
||||
ctx.SetSMTPLastResponse(clientID, res)
|
||||
return nil
|
||||
}
|
||||
|
||||
func smtpClientAuthenticatesWithBadPassword(bddUserID string) error {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
res := ctx.GetSMTPClient("smtp").Login(account.Address(), "you shall not pass!")
|
||||
ctx.SetSMTPLastResponse("smtp", res)
|
||||
return nil
|
||||
}
|
||||
|
||||
func smtpClientAuthenticatesWithUsernameAndPassword(bddUserID, password string) error {
|
||||
res := ctx.GetSMTPClient("smtp").Login(bddUserID, password)
|
||||
ctx.SetSMTPLastResponse("smtp", res)
|
||||
return nil
|
||||
}
|
||||
|
||||
func smtpClientLogsOut() error {
|
||||
res := ctx.GetSMTPClient("smtp").Logout()
|
||||
ctx.SetSMTPLastResponse("smtp", res)
|
||||
return nil
|
||||
}
|
||||
|
||||
func smtpClientSendsMessage(message *gherkin.DocString) error {
|
||||
return smtpClientNamedSendsMessage("smtp", message)
|
||||
}
|
||||
|
||||
func smtpClientSendsEHLO() error {
|
||||
res := ctx.GetSMTPClient("smtp").SendCommands("EHLO ateist.test")
|
||||
ctx.SetSMTPLastResponse("smtp", res)
|
||||
return nil
|
||||
}
|
||||
|
||||
func smtpClientNamedSendsMessage(clientID string, message *gherkin.DocString) error {
|
||||
return smtpClientNamedSendsMessageWithBCC(clientID, "", message)
|
||||
}
|
||||
|
||||
func smtpClientSendsMessageWithBCC(bcc string, message *gherkin.DocString) error {
|
||||
return smtpClientNamedSendsMessageWithBCC("smtp", bcc, message)
|
||||
}
|
||||
|
||||
func smtpClientNamedSendsMessageWithBCC(clientID, bcc string, message *gherkin.DocString) error {
|
||||
res := ctx.GetSMTPClient(clientID).SendMail(strings.NewReader(message.Content), bcc)
|
||||
ctx.SetSMTPLastResponse(clientID, res)
|
||||
return nil
|
||||
}
|
||||
41
test/smtp_checks_test.go
Normal file
41
test/smtp_checks_test.go
Normal file
@ -0,0 +1,41 @@
|
||||
// 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 tests
|
||||
|
||||
import (
|
||||
"github.com/cucumber/godog"
|
||||
)
|
||||
|
||||
func SMTPChecksFeatureContext(s *godog.Suite) {
|
||||
s.Step(`^SMTP response is "([^"]*)"$`, smtpResponseIs)
|
||||
s.Step(`^SMTP response to "([^"]*)" is "([^"]*)"$`, smtpResponseNamedIs)
|
||||
}
|
||||
|
||||
func smtpResponseIs(expectedResponse string) error {
|
||||
return smtpResponseNamedIs("smtp", expectedResponse)
|
||||
}
|
||||
|
||||
func smtpResponseNamedIs(clientID, expectedResponse string) error {
|
||||
res := ctx.GetSMTPLastResponse(clientID)
|
||||
if expectedResponse == "OK" {
|
||||
res.AssertOK()
|
||||
} else {
|
||||
res.AssertError(expectedResponse)
|
||||
}
|
||||
return ctx.GetTestingError()
|
||||
}
|
||||
55
test/smtp_setup_test.go
Normal file
55
test/smtp_setup_test.go
Normal file
@ -0,0 +1,55 @@
|
||||
// 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 tests
|
||||
|
||||
import (
|
||||
"github.com/cucumber/godog"
|
||||
)
|
||||
|
||||
func SMTPSetupFeatureContext(s *godog.Suite) {
|
||||
s.Step(`^there is SMTP client logged in as "([^"]*)"$`, thereIsSMTPClientLoggedInAs)
|
||||
s.Step(`^there is SMTP client "([^"]*)" logged in as "([^"]*)"$`, thereIsSMTPClientNamedLoggedInAs)
|
||||
s.Step(`^there is SMTP client logged in as "([^"]*)" with address "([^"]*)"$`, thereIsSMTPClientLoggedInAsWithAddress)
|
||||
s.Step(`^there is SMTP client "([^"]*)" logged in as "([^"]*)" with address "([^"]*)"$`, thereIsSMTPClientNamedLoggedInAsWithAddress)
|
||||
}
|
||||
|
||||
func thereIsSMTPClientLoggedInAs(bddUserID string) error {
|
||||
return thereIsSMTPClientNamedLoggedInAs("smtp", bddUserID)
|
||||
}
|
||||
|
||||
func thereIsSMTPClientNamedLoggedInAs(clientID, bddUserID string) error {
|
||||
account := ctx.GetTestAccount(bddUserID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
ctx.GetSMTPClient(clientID).Login(account.Address(), account.BridgePassword()).AssertOK()
|
||||
return ctx.GetTestingError()
|
||||
}
|
||||
|
||||
func thereIsSMTPClientLoggedInAsWithAddress(bddUserID, bddAddressID string) error {
|
||||
return thereIsSMTPClientNamedLoggedInAsWithAddress("smtp", bddUserID, bddAddressID)
|
||||
}
|
||||
|
||||
func thereIsSMTPClientNamedLoggedInAsWithAddress(clientID, bddUserID, bddAddressID string) error {
|
||||
account := ctx.GetTestAccountWithAddress(bddUserID, bddAddressID)
|
||||
if account == nil {
|
||||
return godog.ErrPending
|
||||
}
|
||||
ctx.GetSMTPClient(clientID).Login(account.Address(), account.BridgePassword()).AssertOK()
|
||||
return ctx.GetTestingError()
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user