mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 04:36:43 +00:00
test(BRIDGE-131): Integration tests for messages from Proton <-> Gmail
This commit is contained in:
committed by
Atanas Janeshliev
parent
040d887aae
commit
b481ce2203
149
tests/utils/gmail/service.go
Normal file
149
tests/utils/gmail/service.go
Normal file
@ -0,0 +1,149 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.Bridge.
|
||||
//
|
||||
// Proton Mail 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.
|
||||
//
|
||||
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/tests/utils/gmail/tokenservice"
|
||||
"github.com/cucumber/godog"
|
||||
"google.golang.org/api/gmail/v1"
|
||||
"google.golang.org/api/option"
|
||||
)
|
||||
|
||||
const DEBUG = false
|
||||
const GmailUserID = "me"
|
||||
|
||||
func getGmailService() *gmail.Service {
|
||||
ctx := context.Background()
|
||||
|
||||
gmailClient, err := tokenservice.LoadGmailClient(ctx)
|
||||
if err != nil {
|
||||
log.Fatalf("unable to retrieve gmail http client: %v", err)
|
||||
}
|
||||
|
||||
gmailService, err := gmail.NewService(ctx, option.WithHTTPClient(gmailClient))
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to retrieve Gmail client: %v", err)
|
||||
}
|
||||
|
||||
return gmailService
|
||||
}
|
||||
|
||||
func ExternalSendEmail(from, to string, message *godog.DocString) error {
|
||||
srv := getGmailService()
|
||||
|
||||
var msg gmail.Message
|
||||
|
||||
msgStr := []byte(
|
||||
"From: " + from + " \n" +
|
||||
"To: " + to + " \n" +
|
||||
message.Content)
|
||||
|
||||
msg.Raw = base64.URLEncoding.EncodeToString(msgStr)
|
||||
|
||||
_, err := srv.Users.Messages.Send(GmailUserID, &msg).Do()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func FetchMessageBySubjectAndSender(subject, sender, state string) (*gmail.Message, error) {
|
||||
srv := getGmailService()
|
||||
|
||||
var q string
|
||||
switch state {
|
||||
case "read":
|
||||
q = fmt.Sprintf("(is:read in:inbox OR in:spam) subject:%q from:%q newer:1", subject, sender)
|
||||
case "unread":
|
||||
q = fmt.Sprintf("(is:unread in:inbox OR in:spam) subject:%q from:%q newer:1", subject, sender)
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid state argument, must be 'read' or 'unread'")
|
||||
}
|
||||
|
||||
if DEBUG {
|
||||
fmt.Println("Gmail API Query:", q)
|
||||
}
|
||||
|
||||
r, err := srv.Users.Messages.List(GmailUserID).Q(q).Do()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to retrieve %s messages with subject: %q and sender: %q with error: %v", state, subject, sender, err)
|
||||
}
|
||||
|
||||
if len(r.Messages) == 0 {
|
||||
return nil, fmt.Errorf("no %s messages found with subject: %q and sender: %q", state, subject, sender)
|
||||
}
|
||||
|
||||
newestMessageID := r.Messages[0].Id
|
||||
newestMessage, err := srv.Users.Messages.Get(GmailUserID, newestMessageID).Do()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to retrieve details of the newest message: %v", err)
|
||||
}
|
||||
|
||||
if DEBUG {
|
||||
fmt.Println("Email Subject:", getEmailHeader(newestMessage, "Subject"))
|
||||
fmt.Println("Email Sender:", getEmailHeader(newestMessage, "From"))
|
||||
}
|
||||
|
||||
return newestMessage, nil
|
||||
}
|
||||
|
||||
func getEmailHeader(message *gmail.Message, headerName string) string {
|
||||
if message != nil && message.Payload != nil && message.Payload.Headers != nil {
|
||||
for _, header := range message.Payload.Headers {
|
||||
if header.Name == headerName {
|
||||
return header.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
return "Header not found"
|
||||
}
|
||||
|
||||
func GetRawMessage(message *gmail.Message) (string, error) {
|
||||
srv := getGmailService()
|
||||
msg, err := srv.Users.Messages.Get(GmailUserID, message.Id).Format("raw").Do()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
decodedMsg, err := base64.URLEncoding.DecodeString(msg.Raw)
|
||||
|
||||
return string(decodedMsg), err
|
||||
}
|
||||
|
||||
func DeleteAllMessages() {
|
||||
srv := getGmailService()
|
||||
|
||||
labels := []string{"INBOX", "SENT", "DRAFT", "SPAM", "TRASH"}
|
||||
|
||||
for _, label := range labels {
|
||||
msgs, err := srv.Users.Messages.List(GmailUserID).LabelIds(label).Do()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, m := range msgs.Messages {
|
||||
_ = srv.Users.Messages.Delete(GmailUserID, m.Id).Do()
|
||||
}
|
||||
}
|
||||
}
|
||||
183
tests/utils/gmail/tokenservice/tokenservice.go
Normal file
183
tests/utils/gmail/tokenservice/tokenservice.go
Normal file
@ -0,0 +1,183 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.Bridge.
|
||||
//
|
||||
// Proton Mail 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.
|
||||
//
|
||||
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package tokenservice
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
"google.golang.org/api/gmail/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
NexusTestingPwd = "NEXUS_TESTING_PWD"
|
||||
NexusTestingUser = "NEXUS_TESTING_USER"
|
||||
)
|
||||
|
||||
var nexusAccessVerificationURL = os.Getenv("NEXUS_ACCESS_VERIFICATION_URL")
|
||||
var nexusCredentialsURL = os.Getenv("NEXUS_GMAIL_CREDENTIALS_URL")
|
||||
var nexusTokenURL = os.Getenv("NEXUS_GMAIL_TOKEN_URL")
|
||||
|
||||
var gmailScopes = []string{gmail.GmailComposeScope, gmail.GmailInsertScope, gmail.GmailLabelsScope, gmail.MailGoogleComScope, gmail.GmailMetadataScope, gmail.GmailModifyScope, gmail.GmailSendScope}
|
||||
|
||||
func fetchBytes(url string) ([]byte, error) {
|
||||
res, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to fetch data from url %v: %v", url, err)
|
||||
}
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read response body for url %v: %v", url, err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func fetchConfig() (*oauth2.Config, error) {
|
||||
data, err := fetchBytes(nexusCredentialsURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to fetch credentials: %v", err)
|
||||
}
|
||||
|
||||
config, err := google.ConfigFromJSON(data, gmailScopes...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse credentials to gmail config: %v", err)
|
||||
}
|
||||
|
||||
return config, err
|
||||
}
|
||||
|
||||
func fetchToken() (*oauth2.Token, error) {
|
||||
data, err := fetchBytes(nexusTokenURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to fetch token: %v", err)
|
||||
}
|
||||
|
||||
token := &oauth2.Token{}
|
||||
if err = json.Unmarshal(data, token); err != nil {
|
||||
return nil, fmt.Errorf("error when unmarshaling token: %v", err)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func refreshToken(config *oauth2.Config, token *oauth2.Token) (*oauth2.Token, error) {
|
||||
ctx := context.Background()
|
||||
tokenSource := config.TokenSource(ctx, token)
|
||||
newToken, err := tokenSource.Token()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error when refreshing access token: %v", err)
|
||||
}
|
||||
return newToken, nil
|
||||
}
|
||||
|
||||
func getNexusCreds() string {
|
||||
credentials := os.Getenv(NexusTestingUser) + ":" + os.Getenv(NexusTestingPwd)
|
||||
return base64.StdEncoding.EncodeToString([]byte(credentials))
|
||||
}
|
||||
|
||||
func pushToNexus(url string, data []byte) error {
|
||||
req, err := http.NewRequest("PUT", url, bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating put request to nexus: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
encodedCredentials := getNexusCreds()
|
||||
req.Header.Set("Authorization", "Basic "+encodedCredentials)
|
||||
|
||||
client := http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error making put request to nexus: %v", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("unexpected status code when making put request to nexus: %v", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func uploadTokenToNexus(token *oauth2.Token) error {
|
||||
jsonData, err := json.Marshal(token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error when encoding access token to json: %v", err)
|
||||
}
|
||||
|
||||
return pushToNexus(nexusTokenURL, jsonData)
|
||||
}
|
||||
|
||||
func verifyNexusAccess() error {
|
||||
return pushToNexus(nexusAccessVerificationURL, nil)
|
||||
}
|
||||
|
||||
func checkTokenValidityAndRefresh(config *oauth2.Config, token *oauth2.Token) (*oauth2.Token, error) {
|
||||
// Validate token (check if it has expired, or is 5 minutes from expiring) and refresh
|
||||
timeTillExpiry := time.Until(token.Expiry)
|
||||
if !token.Valid() || timeTillExpiry < 5*time.Minute {
|
||||
token, err := refreshToken(config, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = uploadTokenToNexus(token); err != nil {
|
||||
return nil, fmt.Errorf("unable to upload token to nexus: %v", err)
|
||||
}
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func LoadGmailClient(ctx context.Context) (*http.Client, error) {
|
||||
err := verifyNexusAccess()
|
||||
if err != nil {
|
||||
log.Fatalf("error occurred when verifying nexus access, check your credentials: %v", err)
|
||||
}
|
||||
|
||||
config, err := fetchConfig()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("issue obtaining oauth config: %v", err)
|
||||
}
|
||||
|
||||
token, err := fetchToken()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("issue obtaining oauth token: %v", err)
|
||||
}
|
||||
|
||||
token, err = checkTokenValidityAndRefresh(config, token)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error checking token validity: %v", err)
|
||||
}
|
||||
|
||||
client := config.Client(ctx, token)
|
||||
return client, nil
|
||||
}
|
||||
Reference in New Issue
Block a user