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
@ -129,13 +129,15 @@ func getFeatureTags() string {
|
||||
|
||||
switch arguments := os.Args; arguments[len(arguments)-1] {
|
||||
case "nightly":
|
||||
tags = ""
|
||||
tags = "~@gmail-integration"
|
||||
case "smoke": // Currently this is just a placeholder, as there are no scenarios tagged with @smoke
|
||||
tags = "@smoke"
|
||||
case "black": // Currently this is just a placeholder, as there are no scenarios tagged with @smoke
|
||||
tags = "~@skip-black"
|
||||
case "gmail-integration":
|
||||
tags = "@gmail-integration"
|
||||
default:
|
||||
tags = "~@regression && ~@smoke" // To exclude more add `&& ~@tag`
|
||||
tags = "~@regression && ~@smoke && ~@gmail-integration" // To exclude more add `&& ~@tag`
|
||||
}
|
||||
|
||||
return tags
|
||||
|
||||
118
tests/external_test.go
Normal file
118
tests/external_test.go
Normal file
@ -0,0 +1,118 @@
|
||||
// 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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/mail"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/gluon/rfc822"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/message"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/message/parser"
|
||||
GmailService "github.com/ProtonMail/proton-bridge/v3/tests/utils/gmail"
|
||||
"github.com/cucumber/godog"
|
||||
)
|
||||
|
||||
func (s *scenario) externalClientSendsTheFollowingMessageFromTo(from, to string, message *godog.DocString) error {
|
||||
return GmailService.ExternalSendEmail(from, to, message)
|
||||
}
|
||||
|
||||
func (s *scenario) externalClientFetchesTheFollowingMessage(subject, sender, state string) error {
|
||||
err := eventually(func() error {
|
||||
_, err := GmailService.FetchMessageBySubjectAndSender(subject, sender, state)
|
||||
return err
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *scenario) externalClientSeesMessageWithStructure(subject, sender, state string, message *godog.DocString) error {
|
||||
err := eventually(func() error {
|
||||
gmailMessage, err := GmailService.FetchMessageBySubjectAndSender(subject, sender, state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var msgStruct MessageStruct
|
||||
if err := json.Unmarshal([]byte(message.Content), &msgStruct); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parsedMessage, err := GmailService.GetRawMessage(gmailMessage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var structs []MessageStruct
|
||||
messageStruct := parseGmail(parsedMessage)
|
||||
structs = append(structs, messageStruct)
|
||||
|
||||
return matchStructureRecursive(structs, msgStruct)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *scenario) externalClientDeletesAllMessages() {
|
||||
GmailService.DeleteAllMessages()
|
||||
}
|
||||
|
||||
func parseGmail(rawMsg string) MessageStruct {
|
||||
msg, err := mail.ReadMessage(strings.NewReader(rawMsg))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var dec mime.WordDecoder
|
||||
decodedSubject, err := dec.DecodeHeader(msg.Header.Get("Subject"))
|
||||
if err != nil {
|
||||
decodedSubject = msg.Header.Get("Subject")
|
||||
}
|
||||
|
||||
parser, err := parser.New(strings.NewReader(rawMsg))
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("parser error: %e", err))
|
||||
}
|
||||
|
||||
m, err := message.ParseWithParser(parser, true)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("parser with parser: %e", err))
|
||||
}
|
||||
|
||||
var body string
|
||||
switch {
|
||||
case m.MIMEType == rfc822.TextPlain:
|
||||
body = strings.TrimSpace(string(m.PlainBody))
|
||||
case m.MIMEType == rfc822.MultipartMixed:
|
||||
_, body, _ = strings.Cut(string(m.MIMEBody), "\r\n\r\n")
|
||||
default:
|
||||
body = strings.TrimSpace(string(m.RichBody))
|
||||
}
|
||||
|
||||
// There might be an issue with the dates if we end up using them
|
||||
return MessageStruct{
|
||||
From: msg.Header.Get("From"),
|
||||
To: msg.Header.Get("To"),
|
||||
CC: msg.Header.Get("CC"),
|
||||
BCC: msg.Header.Get("BCC"),
|
||||
Subject: decodedSubject,
|
||||
Date: msg.Header.Get("Date"),
|
||||
Content: parseMessageSection([]byte(strings.TrimSpace(rawMsg)), strings.TrimSpace(body)),
|
||||
}
|
||||
}
|
||||
2288
tests/features/external/html_external_to_proton.feature
vendored
Normal file
2288
tests/features/external/html_external_to_proton.feature
vendored
Normal file
File diff suppressed because one or more lines are too long
1536
tests/features/external/html_proton_to_external.feature
vendored
Normal file
1536
tests/features/external/html_proton_to_external.feature
vendored
Normal file
File diff suppressed because one or more lines are too long
283
tests/features/external/plain_external_to_proton.feature
vendored
Normal file
283
tests/features/external/plain_external_to_proton.feature
vendored
Normal file
@ -0,0 +1,283 @@
|
||||
@gmail-integration
|
||||
Feature: External sender to Proton recipient sending a plain text message
|
||||
Background:
|
||||
Given there exists an account with username "[user:user]" and password "password"
|
||||
Then it succeeds
|
||||
When bridge starts
|
||||
And the user logs in with username "[user:user]" and password "password"
|
||||
Then it succeeds
|
||||
|
||||
Scenario: Plain text message sent from External to Internal
|
||||
Given external client sends the following message from "auto.bridge.qa@gmail.com" to "[user:user]@[domain]":
|
||||
"""
|
||||
From: <auto.bridge.qa@gmail.com>
|
||||
To: <[user:user]@[domain]>
|
||||
Content-Type: text/plain; charset=UTF-8; format=flowed
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Subject: Hello World!
|
||||
|
||||
hello
|
||||
|
||||
"""
|
||||
Then it succeeds
|
||||
When user "[user:user]" connects and authenticates IMAP client "1"
|
||||
Then IMAP client "1" eventually sees the following messages in "Inbox":
|
||||
| from | to | subject | body |
|
||||
| auto.bridge.qa@gmail.com | [user:user]@[domain] | Hello World! | hello |
|
||||
And IMAP client "1" eventually sees the following message in "Inbox" with this structure:
|
||||
"""
|
||||
{
|
||||
"from": "auto.bridge.qa@gmail.com",
|
||||
"to": "[user:user]@[domain]",
|
||||
"subject": "Hello World!",
|
||||
"content": {
|
||||
"content-type": "text/plain",
|
||||
"content-type-charset": "utf-8",
|
||||
"transfer-encoding": "quoted-printable",
|
||||
"body-is": "hello"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: Plain message with Foreign/Nonascii chars in Subject and Body from External to Internal
|
||||
Given external client sends the following message from "auto.bridge.qa@gmail.com" to "[user:user]@[domain]":
|
||||
"""
|
||||
To: <[user:user]@[domain]>
|
||||
From: Bridge Automation <auto.bridge.qa@gmail.com>
|
||||
Subject: =?UTF-8?B?U3Vias61zq3Pgs+EIMK2IMOEIMOI?=
|
||||
Content-Type: text/plain; charset=UTF-8; format=flowed
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Subjεέςτ ¶ Ä È
|
||||
|
||||
"""
|
||||
Then it succeeds
|
||||
When user "[user:user]" connects and authenticates IMAP client "1"
|
||||
Then IMAP client "1" eventually sees the following messages in "Inbox":
|
||||
| from | to | subject | body |
|
||||
| auto.bridge.qa@gmail.com | [user:user]@[domain] | Subjεέςτ ¶ Ä È | Subjεέςτ ¶ Ä È |
|
||||
And IMAP client "1" eventually sees the following message in "Inbox" with this structure:
|
||||
"""
|
||||
{
|
||||
"from": "auto.bridge.qa@gmail.com",
|
||||
"to": "[user:user]@[domain]",
|
||||
"subject": "Subjεέςτ ¶ Ä È",
|
||||
"content": {
|
||||
"content-type": "text/plain",
|
||||
"content-type-charset": "utf-8",
|
||||
"transfer-encoding": "quoted-printable",
|
||||
"body-is": "Subjεέςτ ¶ Ä È"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: Plain message with numbering/ordering in Body from External to Internal
|
||||
Given external client sends the following message from "auto.bridge.qa@gmail.com" to "[user:user]@[domain]":
|
||||
"""
|
||||
To: <[user:user]@[domain]>
|
||||
From: Bridge Automation <auto.bridge.qa@gmail.com>
|
||||
Subject: Message with Numbering/Ordering in Body
|
||||
Content-Type: text/plain; charset=UTF-8; format=flowed
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
**Ordering
|
||||
|
||||
* *Bullet*1
|
||||
o Bullet 1.1
|
||||
* Bullet 2
|
||||
o Bullet 2.1
|
||||
o *Bullet 2.2*
|
||||
+ /Bullet 2.2.1/
|
||||
o Bullet 2.3
|
||||
* */Bullet 3/*
|
||||
|
||||
Numbering
|
||||
|
||||
1. *Number 1*
|
||||
1. */Number/**1.1*
|
||||
2. Number 2
|
||||
|
||||
1. */Number 2.1/*
|
||||
2. Number 2.2
|
||||
1. Number 2.2.1
|
||||
3. Number 2.3
|
||||
|
||||
3. /Number 3/
|
||||
|
||||
End
|
||||
"""
|
||||
Then it succeeds
|
||||
When user "[user:user]" connects and authenticates IMAP client "1"
|
||||
Then IMAP client "1" eventually sees the following messages in "Inbox":
|
||||
| from | to | subject |
|
||||
| auto.bridge.qa@gmail.com | [user:user]@[domain] | Message with Numbering/Ordering in Body |
|
||||
And IMAP client "1" eventually sees the following message in "Inbox" with this structure:
|
||||
"""
|
||||
{
|
||||
"from": "auto.bridge.qa@gmail.com",
|
||||
"to": "[user:user]@[domain]",
|
||||
"subject": "Message with Numbering/Ordering in Body",
|
||||
"content": {
|
||||
"content-type": "text/plain",
|
||||
"content-type-charset": "utf-8",
|
||||
"transfer-encoding": "quoted-printable",
|
||||
"body-is": "**Ordering\r\n\r\n* *Bullet*1\r\n o Bullet 1.1\r\n* Bullet 2\r\n o Bullet 2.1\r\n o *Bullet 2.2*\r\n + /Bullet 2.2.1/\r\n o Bullet 2.3\r\n* */Bullet 3/*\r\n\r\nNumbering\r\n\r\n1. *Number 1*\r\n 1. */Number/**1.1*\r\n2. Number 2\r\n\r\n 1. */Number 2.1/*\r\n 2. Number 2.2\r\n 1. Number 2.2.1\r\n 3. Number 2.3\r\n\r\n3. /Number 3/\r\n\r\nEnd"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: Plain text message with multiple attachments from External to Internal
|
||||
Given external client sends the following message from "auto.bridge.qa@gmail.com" to "[user:user]@[domain]":
|
||||
"""
|
||||
Content-Type: multipart/mixed; boundary="------------WI90RPIYF20K6dGXjs7dm2mi"
|
||||
Subject: Plain message with different attachments
|
||||
|
||||
This is a multi-part message in MIME format.
|
||||
--------------WI90RPIYF20K6dGXjs7dm2mi
|
||||
Content-Type: text/plain; charset=UTF-8;
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
Hello, this is a Plain message with different attachments.
|
||||
|
||||
--------------WI90RPIYF20K6dGXjs7dm2mi
|
||||
Content-Type: text/html; charset=UTF-8; name="index.html"
|
||||
Content-Disposition: attachment; filename="index.html"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
PCFET0NUWVBFIGh0bWw+
|
||||
--------------WI90RPIYF20K6dGXjs7dm2mi
|
||||
Content-Type: text/xml; charset=UTF-8; name="testxml.xml"
|
||||
Content-Disposition: attachment; filename="testxml.xml"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
PD94bWwgdmVyc2lvbj0iMS4xIj8+PCFET0NUWVBFIF9bPCFFTEVNRU5UIF8gRU1QVFk+XT48
|
||||
Xy8+
|
||||
--------------WI90RPIYF20K6dGXjs7dm2mi
|
||||
Content-Type: text/plain; charset=UTF-8; name="text file.txt"
|
||||
Content-Disposition: attachment; filename="text file.txt"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
dGV4dCBmaWxl
|
||||
--------------WI90RPIYF20K6dGXjs7dm2mi
|
||||
"""
|
||||
Then it succeeds
|
||||
When user "[user:user]" connects and authenticates IMAP client "1"
|
||||
Then IMAP client "1" eventually sees the following messages in "Inbox":
|
||||
| from | to | subject |
|
||||
| auto.bridge.qa@gmail.com | [user:user]@[domain] | Plain message with different attachments |
|
||||
And IMAP client "1" eventually sees the following message in "Inbox" with this structure:
|
||||
"""
|
||||
{
|
||||
"from": "auto.bridge.qa@gmail.com",
|
||||
"to": "[user:user]@[domain]",
|
||||
"subject": "Plain message with different attachments",
|
||||
"content": {
|
||||
"content-type": "multipart/mixed",
|
||||
"sections": [
|
||||
{
|
||||
"content-type": "text/plain",
|
||||
"content-type-charset": "utf-8",
|
||||
"transfer-encoding": "quoted-printable",
|
||||
"body-is": "Hello, this is a Plain message with different attachments."
|
||||
},
|
||||
{
|
||||
"content-type": "text/plain",
|
||||
"content-type-name": "text file.txt",
|
||||
"content-disposition": "attachment",
|
||||
"content-disposition-filename": "text file.txt",
|
||||
"transfer-encoding": "base64",
|
||||
"body-is": "dGV4dCBmaWxl"
|
||||
},
|
||||
{
|
||||
"content-type": "text/html",
|
||||
"content-type-name": "index.html",
|
||||
"content-disposition": "attachment",
|
||||
"content-disposition-filename": "index.html",
|
||||
"transfer-encoding": "base64",
|
||||
"body-is": "PCFET0NUWVBFIGh0bWw+"
|
||||
},
|
||||
{
|
||||
"content-type": "text/xml",
|
||||
"content-type-name": "testxml.xml",
|
||||
"content-disposition": "attachment",
|
||||
"content-disposition-filename": "testxml.xml",
|
||||
"transfer-encoding": "base64",
|
||||
"body-is": "PD94bWwgdmVyc2lvbj0iMS4xIj8+PCFET0NUWVBFIF9bPCFFTEVNRU5UIF8gRU1QVFk+XT48Xy8+"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: Plain message with multiple inline images from External to Internal
|
||||
Given external client sends the following message from "auto.bridge.qa@gmail.com" to "[user:user]@[domain]":
|
||||
"""
|
||||
To: <[user:user]@[domain]>
|
||||
From: <auto.bridge.qa@gmail.com>
|
||||
Subject: Plain message with multiple inline images to Internal
|
||||
Content-Type: text/plain; charset=UTF-8; format=flowed
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
Plain message with image 1 multiple image 2 inline image 3 images.
|
||||
|
||||
"""
|
||||
Then it succeeds
|
||||
When user "[user:user]" connects and authenticates IMAP client "1"
|
||||
Then IMAP client "1" eventually sees the following messages in "Inbox":
|
||||
| from | to | subject |
|
||||
| auto.bridge.qa@gmail.com | [user:user]@[domain] | Plain message with multiple inline images to Internal |
|
||||
And IMAP client "1" eventually sees the following message in "Inbox" with this structure:
|
||||
"""
|
||||
{
|
||||
"from": "auto.bridge.qa@gmail.com",
|
||||
"to": "[user:user]@[domain]",
|
||||
"subject": "Plain message with multiple inline images to Internal",
|
||||
"content": {
|
||||
"content-type": "text/plain",
|
||||
"content-type-charset": "utf-8",
|
||||
"transfer-encoding": "quoted-printable",
|
||||
"body-is": "Plain message with image 1 multiple image 2 inline image 3 images.",
|
||||
"body-contains": "",
|
||||
"sections": []
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: Plain text message with a large attachment from External to Internal
|
||||
Given external client sends the following message from "auto.bridge.qa@gmail.com" to "[user:user]@[domain]":
|
||||
"""
|
||||
Content-Type: multipart/mixed; boundary="------------k0Z3FJiZsGaSFqdJGsr0Oml6"
|
||||
To: <[user:user]@[domain]>
|
||||
From: Bridge Automation <auto.bridge.qa@gmail.com>
|
||||
Subject: Plain message with a large attachment
|
||||
|
||||
This is a multi-part message in MIME format.
|
||||
--------------k0Z3FJiZsGaSFqdJGsr0Oml6
|
||||
Content-Type: text/plain; charset=UTF-8; format=flowed
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
Hello, this is a plain message with a large attachment.
|
||||
|
||||
--------------k0Z3FJiZsGaSFqdJGsr0Oml6
|
||||
Content-Type: application/msword; name="testDoc.doc"
|
||||
Content-Disposition: attachment; filename="testDoc.doc"
|
||||
Content-Transfer-Encoding: base64
|
||||
--------------k0Z3FJiZsGaSFqdJGsr0Oml6--
|
||||
"""
|
||||
Then it succeeds
|
||||
When user "[user:user]" connects and authenticates IMAP client "1"
|
||||
Then IMAP client "1" eventually sees the following messages in "Inbox":
|
||||
| from | to | subject |
|
||||
| auto.bridge.qa@gmail.com | [user:user]@[domain] | Plain message with a large attachment |
|
||||
And IMAP client "1" eventually sees the following message in "Inbox" with this structure:
|
||||
"""
|
||||
{
|
||||
"from": "auto.bridge.qa@gmail.com",
|
||||
"to": "[user:user]@[domain]",
|
||||
"subject": "Plain message with a large attachment",
|
||||
"content": {
|
||||
"content-type": "text/plain",
|
||||
"content-type-charset": "utf-8"
|
||||
}
|
||||
}
|
||||
"""
|
||||
2586
tests/features/external/plain_proton_to_external.feature
vendored
Normal file
2586
tests/features/external/plain_proton_to_external.feature
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -190,6 +190,12 @@ func (s *scenario) steps(ctx *godog.ScenarioContext) {
|
||||
ctx.Step(`^SMTP client "([^"]*)" sends the following EML "([^"]*)" from "([^"]*)" to "([^"]*)"$`, s.smtpClientSendsTheFollowingEmlFromTo)
|
||||
ctx.Step(`^SMTP client "([^"]*)" logs out$`, s.smtpClientLogsOut)
|
||||
|
||||
// ==== EXTERNAL ====
|
||||
ctx.Step(`^external client deletes all messages`, s.externalClientDeletesAllMessages)
|
||||
ctx.Step(`^external client sends the following message from "([^"]*)" to "([^"]*)":$`, s.externalClientSendsTheFollowingMessageFromTo)
|
||||
ctx.Step(`^external client fetches the following message with subject "([^"]*)" and sender "([^"]*)" and state "([^"]*)"$`, s.externalClientFetchesTheFollowingMessage)
|
||||
ctx.Step(`^external client fetches the following message with subject "([^"]*)" and sender "([^"]*)" and state "([^"]*)" with this structure:$`, s.externalClientSeesMessageWithStructure)
|
||||
|
||||
// ==== TELEMETRY ====
|
||||
ctx.Step(`^bridge eventually sends the following heartbeat:$`, s.bridgeEventuallySendsTheFollowingHeartbeat)
|
||||
ctx.Step(`^bridge needs to send heartbeat`, s.bridgeNeedsToSendHeartbeat)
|
||||
|
||||
@ -411,6 +411,89 @@ func matchContent(have MessageSection, want MessageSection) (bool, string) {
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func matchStructureRecursive(have []MessageStruct, want MessageStruct) error {
|
||||
mismatches := make([]string, 0)
|
||||
for _, msg := range have {
|
||||
if want.From != "" && msg.From != want.From {
|
||||
mismatches = append(mismatches, "From")
|
||||
continue
|
||||
}
|
||||
if want.To != "" && msg.To != want.To {
|
||||
mismatches = append(mismatches, "To")
|
||||
continue
|
||||
}
|
||||
if want.BCC != "" && msg.BCC != want.BCC {
|
||||
mismatches = append(mismatches, "BCC")
|
||||
continue
|
||||
}
|
||||
if want.CC != "" && msg.CC != want.CC {
|
||||
mismatches = append(mismatches, "CC")
|
||||
continue
|
||||
}
|
||||
if want.Subject != "" && msg.Subject != want.Subject {
|
||||
mismatches = append(mismatches, "Subject")
|
||||
continue
|
||||
}
|
||||
if want.Date != "" && want.Date != msg.Date {
|
||||
mismatches = append(mismatches, "Date")
|
||||
continue
|
||||
}
|
||||
|
||||
if ok, mismatch := matchContentRecursive(msg.Content, want.Content); !ok {
|
||||
mismatches = append(mismatches, "Content: "+mismatch)
|
||||
continue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("missing messages: have %#v, want %#v with mismatch list %#v", have, want, mismatches)
|
||||
}
|
||||
|
||||
func matchContentRecursive(have MessageSection, want MessageSection) (bool, string) {
|
||||
if want.ContentType != "" && !strings.EqualFold(want.ContentType, have.ContentType) {
|
||||
return false, "ContentType"
|
||||
}
|
||||
if want.ContentTypeBoundary != "" && !strings.EqualFold(want.ContentTypeBoundary, have.ContentTypeBoundary) {
|
||||
return false, "ContentTypeBoundary"
|
||||
}
|
||||
if want.ContentTypeCharset != "" && !strings.EqualFold(want.ContentTypeCharset, have.ContentTypeCharset) {
|
||||
return false, "ContentTypeCharset"
|
||||
}
|
||||
if want.ContentTypeName != "" && !strings.EqualFold(want.ContentTypeName, have.ContentTypeName) {
|
||||
return false, "ContentTypeName"
|
||||
}
|
||||
if want.ContentDisposition != "" && !strings.EqualFold(want.ContentDisposition, have.ContentDisposition) {
|
||||
return false, "ContentDisposition"
|
||||
}
|
||||
if want.ContentDispositionFilename != "" && !strings.EqualFold(want.ContentDispositionFilename, have.ContentDispositionFilename) {
|
||||
return false, "ContentDispositionFilename"
|
||||
}
|
||||
if want.TransferEncoding != "" && !strings.EqualFold(want.TransferEncoding, have.TransferEncoding) {
|
||||
return false, "TransferEncoding"
|
||||
}
|
||||
if want.BodyContains != "" && !strings.Contains(strings.TrimSpace(have.BodyContains), strings.TrimSpace(want.BodyContains)) {
|
||||
return false, "BodyContains"
|
||||
}
|
||||
if want.BodyIs != "" && strings.TrimSpace(have.BodyIs) != strings.TrimSpace(want.BodyIs) {
|
||||
return false, "BodyIs"
|
||||
}
|
||||
|
||||
for _, wantSection := range want.Sections {
|
||||
didPass := false
|
||||
for _, haveSection := range have.Sections {
|
||||
ok, _ := matchContentRecursive(haveSection, wantSection)
|
||||
if ok {
|
||||
didPass = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !didPass {
|
||||
return false, "recursive mismatch found"
|
||||
}
|
||||
}
|
||||
|
||||
return true, ""
|
||||
}
|
||||
|
||||
type Mailbox struct {
|
||||
Name string `bdd:"name"`
|
||||
Total int `bdd:"total"`
|
||||
|
||||
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