Skip to content

Commit

Permalink
ci(e2e): Add end-to-end tests with the store and contracts backend mo…
Browse files Browse the repository at this point in the history
…cks (#345)

This PR augments the end to end tests by adding test cases for the
purchasing workflow, following a similar recipe to the existing
"organization provided" and 'manually input" token test scenarios.

We add one more function to the `end_to_end_test.dart` file, handling
the automation of the referred workflow, triggering a MS Store purchase
action. That requires both the MS Store mock as well as the Contracts
backend mock. Those are started by the test code (Go) prior to running
the Ubuntu Pro for Windows app, ports are selected dynamically and the
final addresses are supplied to the clients of the mock via environment
variables.

That, in turn, requires the MSIX to be built with the mock-aware store
API, as well as a mock-aware CS backend URL (selected by a build tag).

This is basically the contents I presented in one Desktop Team pulse
review demo, but augmented with two simulated purchase failure scenarios
(due CS backend outage or MS Store internal failure) and adapted to the
changes introduced in #339 .

One race condition in the storemockserver was found and fixed as part of
this journey.
  • Loading branch information
CarlosNihelton authored Oct 21, 2023
2 parents 362328c + cd08f08 commit 0eaef3c
Show file tree
Hide file tree
Showing 16 changed files with 286 additions and 20 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/qa-azure.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ jobs:
Import-PfxCertificate -Password $pwd -CertStoreLocation Cert:CurrentUser\My -FilePath certificate\certificate.pfx
- name: Build Bundle
run: |
$env:UP4W_TEST_WITH_MS_STORE_MOCK=${{ matrix.mode == 'end_to_end_tests' && '1' || '$null'}}
msbuild `
.\msix\msix.sln `
-target:Build `
Expand Down
12 changes: 8 additions & 4 deletions end-to-end/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,23 @@ go 1.21.3

require (
github.com/canonical/ubuntu-pro-for-windows/common v0.0.0-20230906090052-60fb5d60ada4
github.com/canonical/ubuntu-pro-for-windows/mocks v0.0.0-20231018132816-d78b12b1d065
github.com/stretchr/testify v1.8.4
github.com/ubuntu/decorate v0.0.0-20230905131025-e968fa48a85c
github.com/ubuntu/gowsl v0.0.0-20231004124730-8fd8df02f394
golang.org/x/sys v0.13.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/canonical/ubuntu-pro-for-windows/contractsapi v0.0.0-20230906090052-60fb5d60ada4 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
github.com/spf13/cobra v1.7.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
19 changes: 16 additions & 3 deletions end-to-end/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,33 @@ github.com/0xrawsec/golang-utils v1.3.2 h1:ww4jrtHRSnX9xrGzJYbalx5nXoZewy4zPxiY+
github.com/0xrawsec/golang-utils v1.3.2/go.mod h1:m7AzHXgdSAkFCD9tWWsApxNVxMlyy7anpPVOyT/yM7E=
github.com/canonical/ubuntu-pro-for-windows/common v0.0.0-20230906090052-60fb5d60ada4 h1:8gmzKOf7uRSXl+WfGcQNhf9Ua3Xo5eJzo+6G9nNrXTg=
github.com/canonical/ubuntu-pro-for-windows/common v0.0.0-20230906090052-60fb5d60ada4/go.mod h1:vNexvsl8a0qQmLwFcmF2dPur0FH4jmMJAPH2VnXN+I0=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/canonical/ubuntu-pro-for-windows/contractsapi v0.0.0-20230906090052-60fb5d60ada4 h1:0jVDQ6uSC6scEXX6e83sD6Rw6KhD8/7Vr4Ha1ob2CHw=
github.com/canonical/ubuntu-pro-for-windows/contractsapi v0.0.0-20230906090052-60fb5d60ada4/go.mod h1:lqO8UB33LPVdfiMDMlc1swo3S9bwqhW8DtO75dgTLWo=
github.com/canonical/ubuntu-pro-for-windows/mocks v0.0.0-20231018132816-d78b12b1d065 h1:TZ3D+XHolu1WV9VbEhyJw7kpDWF2LnbEVR0KXifcdIg=
github.com/canonical/ubuntu-pro-for-windows/mocks v0.0.0-20231018132816-d78b12b1d065/go.mod h1:A99+dgUvd12iHGuZKui/Eol0fV3Uc4oOrSzwSKI+/mA=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
Expand All @@ -28,12 +37,16 @@ github.com/ubuntu/decorate v0.0.0-20230905131025-e968fa48a85c h1:jO41xNLddTDkrfz
github.com/ubuntu/decorate v0.0.0-20230905131025-e968fa48a85c/go.mod h1:edGgz97NOqS2oqzbKrZqO9YU9neosRrkEZbVJVQynAA=
github.com/ubuntu/gowsl v0.0.0-20231004124730-8fd8df02f394 h1:DS9wb53gTUxFCPYnhAqOhfdRLsHTL9NpzT+F/D8NwIg=
github.com/ubuntu/gowsl v0.0.0-20231004124730-8fd8df02f394/go.mod h1:gu6CgOaqMFFz1h96UocQwXvRvF6CePIqQnI58DzIg2Q=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
150 changes: 150 additions & 0 deletions end-to-end/purchase_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package endtoend_test

import (
"context"
"fmt"
"os"
"path/filepath"
"testing"
"time"

"github.com/canonical/ubuntu-pro-for-windows/common/golden"
"github.com/canonical/ubuntu-pro-for-windows/mocks/contractserver/contractsmockserver"
"github.com/canonical/ubuntu-pro-for-windows/mocks/storeserver/storemockserver"
"github.com/stretchr/testify/require"
wsl "github.com/ubuntu/gowsl"
"gopkg.in/yaml.v3"
)

const (
contractsEndpointEnv = "UP4W_CONTRACTS_BACKEND_MOCK_ENDPOINT"
storeEndpointEnv = "UP4W_MS_STORE_MOCK_ENDPOINT"
allowPurchaseEnvOverride = "UP4W_ALLOW_STORE_PURCHASE=1"
)

func TestPurchase(t *testing.T) {
type whenToken int
const (
never whenToken = iota
beforeDistroRegistration
afterDistroRegistration
)

// Let's be lazy and don't fall into the risk of changing the function name without updating the places where its name is used.
currentFuncName := t.Name()

testCases := map[string]struct {
whenToStartAgent whenToken
withToken string
csServerDown bool
storeDown bool

wantAttached bool
}{
"Success when applying pro token before registration": {whenToStartAgent: beforeDistroRegistration, wantAttached: true},
"Success when applying pro token after registration": {whenToStartAgent: afterDistroRegistration, wantAttached: true},

"Error due MS Store API failure": {whenToStartAgent: beforeDistroRegistration, storeDown: true},
"Error due Contracts Server Backend down": {whenToStartAgent: afterDistroRegistration, csServerDown: true},
}

for name, tc := range testCases {
tc := tc
t.Run(name, func(t *testing.T) {
testSetup(t)

settings := contractsmockserver.DefaultSettings()

token := os.Getenv(proTokenEnv)
if tc.withToken != "" {
token = tc.withToken
}
require.NotEmpty(t, token, "Setup: provide a Pro token either via UP4W_TEST_PRO_TOKEN environment variable or the test case struct withToken field")
settings.Subscription.OnSuccess.Value = token

cs := contractsmockserver.NewServer(settings)
//nolint:errcheck // Nothing we can do about it
defer cs.Stop()

ctx := context.Background()
contractsCtx, contractsCancel := context.WithCancel(ctx)
defer contractsCancel()

if !tc.csServerDown {
err := cs.Serve(contractsCtx, "localhost:0")
require.NoError(t, err, "Setup: Server should return no error")
}

contractsEndpointEnvOverride := fmt.Sprintf("%s=%s", contractsEndpointEnv, cs.Address())

testData, err := os.ReadFile(filepath.Join(golden.TestFamilyPath(t), "storemock_config.yaml"))
require.NoError(t, err, "Setup: Could not read test fixture input file")

storeSettings := storemockserver.DefaultSettings()
err = yaml.Unmarshal(testData, &storeSettings)
require.NoError(t, err, "Setup: Unmarshalling test data should return no error")

store := storemockserver.NewServer(storeSettings)
//nolint:errcheck // Nothing we can do about it
defer store.Stop()

storeCtx, storeCancel := context.WithCancel(ctx)
defer storeCancel()

if !tc.storeDown {
err = store.Serve(storeCtx, "localhost:0")
require.NoError(t, err, "Setup: Server should return no error")
}

storeEndpointEnvOverride := fmt.Sprintf("%s=%s", storeEndpointEnv, store.Address())

// Either runs the ubuntupro app before...
if tc.whenToStartAgent == beforeDistroRegistration {
cleanup := startAgent(t, ctx, currentFuncName, allowPurchaseEnvOverride, contractsEndpointEnvOverride, storeEndpointEnvOverride)
defer cleanup()
}

// Distro setup
name := registerFromTestImage(t, ctx)
d := wsl.NewDistro(ctx, name)

defer func() {
if t.Failed() {
logWslProServiceJournal(t, ctx, d)
}
}()

out, err := d.Command(ctx, "exit 0").CombinedOutput()
require.NoErrorf(t, err, "Setup: could not wake distro up: %v. %s", err, out)

// ... or after registration, but never both.
if tc.whenToStartAgent == afterDistroRegistration {
cleanup := startAgent(t, ctx, currentFuncName, allowPurchaseEnvOverride, contractsEndpointEnvOverride, storeEndpointEnvOverride)
defer cleanup()

out, err = d.Command(ctx, "exit 0").CombinedOutput()
require.NoErrorf(t, err, "Setup: could not wake distro up: %v. %s", err, out)
}

const maxTimeout = 30 * time.Second

if !tc.wantAttached {
time.Sleep(maxTimeout)
proCtx, cancel := context.WithTimeout(ctx, maxTimeout)
defer cancel()
attached, err := distroIsProAttached(t, proCtx, d)
require.NoError(t, err, "could not determine if distro is attached")
require.False(t, attached, "distro should not have been Pro attached")
return
}

require.Eventually(t, func() bool {
attached, err := distroIsProAttached(t, ctx, d)
if err != nil {
t.Logf("could not determine if distro is attached: %v", err)
}
return attached
}, maxTimeout, time.Second, "distro should have been Pro attached")
})
}
}
35 changes: 35 additions & 0 deletions end-to-end/testdata/TestPurchase/storemock_config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
generateuserjwt:
onsuccess:
value: CPP_MOCK_JWT
status: 200
allproducts:
- storeid: 9P25B50XMKXT
title: Annual Subscription (production)
description: To this lovely mock of the production product ID
isinusercollection: false
productkind: Durable
expirationdate: 0001-01-01T00:00:00Z
- storeid: 9N9Q5G4QSMLS
title: Monthly Subscription (created for testing, free)
description: To this lovely mock of the production product ID
isinusercollection: false
productkind: Durable
expirationdate: 0001-01-01T00:00:00Z
- storeid: CPP_MOCK_CONSUMABLE
title: Consume
description: This mock is nice
isinusercollection: false
productkind: Consumable
expirationdate: 0001-01-01T00:00:00Z
- storeid: cannotpurchase
title: Forbidden
description: This product cannot be owned
isinusercollection: false
productkind: Durable
expirationdate: 0001-01-01T00:00:00Z
- storeid: servererror
title: Also forbidden
description: This product always break the server
isinusercollection: false
productkind: Durable
expirationdate: 0001-01-01T00:00:00Z
5 changes: 2 additions & 3 deletions end-to-end/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"github.com/canonical/ubuntu-pro-for-windows/common/wsltestutils"
"github.com/stretchr/testify/require"
"github.com/ubuntu/gowsl"
wsl "github.com/ubuntu/gowsl"
)

func testSetup(t *testing.T) {
Expand Down Expand Up @@ -152,7 +151,7 @@ func stopAgent(ctx context.Context) error {
}

//nolint:revive // testing.T must precede the context
func distroIsProAttached(t *testing.T, ctx context.Context, d wsl.Distro) (bool, error) {
func distroIsProAttached(t *testing.T, ctx context.Context, d gowsl.Distro) (bool, error) {
t.Helper()

out, err := d.Command(ctx, "pro status --format=json").Output()
Expand All @@ -171,7 +170,7 @@ func distroIsProAttached(t *testing.T, ctx context.Context, d wsl.Distro) (bool,
}

//nolint:revive // testing.T must precede the context
func logWslProServiceJournal(t *testing.T, ctx context.Context, d wsl.Distro) {
func logWslProServiceJournal(t *testing.T, ctx context.Context, d gowsl.Distro) {
t.Helper()

out, err := d.Command(ctx, "journalctl -b --no-pager -u wsl-pro.service").CombinedOutput()
Expand Down
4 changes: 3 additions & 1 deletion gui/packages/p4w_ms_store/windows/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,12 @@ set(STORE_API_SRC
# API wrapper implementations
"${STORE_API_DIR}/base/impl/StoreContext.cpp"
"${STORE_API_DIR}/base/impl/StoreContext.hpp"
"${STORE_API_DIR}/base/impl/WinMockContext.cpp"
"${STORE_API_DIR}/base/impl/WinMockContext.hpp"
)
if(DEFINED ENV{UP4W_TEST_WITH_MS_STORE_MOCK})
# TODO: Change to informative warning and update the text once mock client API wrappers are avaiable.
message(FATAL_ERROR "Unsupported build with the MS Store Mock client API due environment variable 'UP4W_TEST_WITH_MS_STORE_MOCK' set to '$ENV{UP4W_TEST_WITH_MS_STORE_MOCK}'.")
message(WARNING "Building the MS Store plugin with the mock client API due environment variable 'UP4W_TEST_WITH_MS_STORE_MOCK' set to '$ENV{UP4W_TEST_WITH_MS_STORE_MOCK}'.")
list(APPEND STORE_API_DEFINES "UP4W_TEST_WITH_MS_STORE_MOCK")
else()
message(STATUS "Building with the production version of MS Store client API. Set the environment variable 'UP4W_TEST_WITH_MS_STORE_MOCK' if you want to build with the mock store API.")
Expand Down
18 changes: 18 additions & 0 deletions gui/packages/ubuntupro/end_to_end/end_to_end_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ void main(List<String> args) {
const testCases = {
'TestOrganizationProvidedToken': testOrganizationProvidedToken,
'TestManualTokenInput': testManualTokenInput,
'TestPurchase': testPurchase,
};

final scenario = args[0];
Expand Down Expand Up @@ -87,3 +88,20 @@ Future<void> testManualTokenInput(WidgetTester tester) async {
l10n = tester.l10n<SubscriptionStatusPage>();
expect(find.text(l10n.manuallyManaged), findsOneWidget);
}

Future<void> testPurchase(WidgetTester tester) async {
await app.main();
await tester.pumpAndSettle();

// The "subscribe now page" is only shown if the GUI communicates with the background agent.
var l10n = tester.l10n<SubscribeNowPage>();
final button = find.text(l10n.subscribeNow);
expect(button, findsOneWidget);

await tester.tap(button);
await tester.pumpAndSettle();

// asserts that we transitioned to the store-managed status page.
l10n = tester.l10n<SubscriptionStatusPage>();
expect(find.text(l10n.storeManaged), findsOneWidget);
}
Loading

0 comments on commit 0eaef3c

Please sign in to comment.