We build too many walls and not enough bridges

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

View 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
View 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
View 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)
}

View 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
}

View 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
}

View 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
}

View 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)
}

View 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
View 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
View 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
View 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
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}