From 2e9763e103baceb09fe35abfa6b73b46c06c16f0 Mon Sep 17 00:00:00 2001 From: Carlos Date: Tue, 17 Oct 2023 14:48:25 -0300 Subject: [PATCH 01/16] Fix gowsl being imported more than once --- end-to-end/utils_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/end-to-end/utils_test.go b/end-to-end/utils_test.go index c1ab3b64b..53ccab988 100644 --- a/end-to-end/utils_test.go +++ b/end-to-end/utils_test.go @@ -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) { @@ -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() @@ -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() From fbcf32b0eece2e813cbc73aa8b17f72e0337cd6b Mon Sep 17 00:00:00 2001 From: Carlos Date: Wed, 4 Oct 2023 21:05:06 -0300 Subject: [PATCH 02/16] storemockserver: Fix race condition on AllProducts A purchase causes one product to change. The background agent can call /products before the update takes place. --- mocks/storeserver/storemockserver/storemockserver.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/mocks/storeserver/storemockserver/storemockserver.go b/mocks/storeserver/storemockserver/storemockserver.go index 247d18b1a..d65538bdf 100644 --- a/mocks/storeserver/storemockserver/storemockserver.go +++ b/mocks/storeserver/storemockserver/storemockserver.go @@ -7,6 +7,7 @@ import ( "fmt" "log/slog" "net/http" + "sync" "time" "github.com/canonical/ubuntu-pro-for-windows/mocks/restserver" @@ -107,6 +108,8 @@ func (s Settings) Unmarshal(in []byte, unmarshaller func(in []byte, out interfac type Server struct { restserver.ServerBase settings Settings + + settingsMu sync.RWMutex } // Product models the interesting properties from the MS StoreProduct type. @@ -218,12 +221,18 @@ func (s *Server) handleGetProducts(w http.ResponseWriter, r *http.Request) { kinds := q[ProductKindsParam] ids := q[ProductIDsParam] var productsFound []Product + + // To avoid race with handlePurchase + s.settingsMu.RLock() + defer s.settingsMu.RUnlock() + for _, p := range s.settings.AllProducts { if slices.Contains(kinds, p.ProductKind) && slices.Contains(ids, p.StoreID) { productsFound = append(productsFound, p) } } + slog.Info(fmt.Sprintf("products found: %v", productsFound)) bs, err := json.Marshal(productsFound) if err != nil { w.WriteHeader(http.StatusInternalServerError) @@ -276,6 +285,9 @@ func (s *Server) handlePurchase(w http.ResponseWriter, r *http.Request) { } year, month, day := time.Now().Date() + + s.settingsMu.Lock() + defer s.settingsMu.Unlock() s.settings.AllProducts[i].ExpirationDate = time.Date(year+1, month, day, 1, 1, 1, 1, time.Local) // one year from now. s.settings.AllProducts[i].IsInUserCollection = true fmt.Fprintf(w, `{%q:%q}`, PurchaseStatusKey, SucceededResult) From 303488e19d7d870eff5fe69da4c26ffc74956aca Mon Sep 17 00:00:00 2001 From: Carlos Date: Wed, 4 Oct 2023 21:03:54 -0300 Subject: [PATCH 03/16] Allows building end-to-end with the store mock --- .github/workflows/qa-azure.yaml | 2 ++ gui/packages/p4w_ms_store/windows/CMakeLists.txt | 4 +++- msix/storeapi/storeapi.vcxproj | 2 ++ tools/build/build-appx.ps1 | 2 +- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/qa-azure.yaml b/.github/workflows/qa-azure.yaml index aa5055e14..696babec8 100644 --- a/.github/workflows/qa-azure.yaml +++ b/.github/workflows/qa-azure.yaml @@ -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 ` diff --git a/gui/packages/p4w_ms_store/windows/CMakeLists.txt b/gui/packages/p4w_ms_store/windows/CMakeLists.txt index 5335858dd..29ed6face 100644 --- a/gui/packages/p4w_ms_store/windows/CMakeLists.txt +++ b/gui/packages/p4w_ms_store/windows/CMakeLists.txt @@ -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.") diff --git a/msix/storeapi/storeapi.vcxproj b/msix/storeapi/storeapi.vcxproj index 8c063c59d..648f0564a 100644 --- a/msix/storeapi/storeapi.vcxproj +++ b/msix/storeapi/storeapi.vcxproj @@ -95,11 +95,13 @@ + + diff --git a/tools/build/build-appx.ps1 b/tools/build/build-appx.ps1 index 11dd2bebb..e9a099c2d 100644 --- a/tools/build/build-appx.ps1 +++ b/tools/build/build-appx.ps1 @@ -4,7 +4,7 @@ #> param ( - [Parameter(Mandatory = $true, HelpMessage = "prodution, end_to_end_tests.")] + [Parameter(Mandatory = $true, HelpMessage = "production, end_to_end_tests.")] [string]$mode ) From 9ee098912b48110feff9f95664be167dd4864888 Mon Sep 17 00:00:00 2001 From: Carlos Date: Wed, 4 Oct 2023 21:05:23 -0300 Subject: [PATCH 04/16] Uses the production product ID --- storeapi/go-wrapper/microsoftstore/store_windows.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storeapi/go-wrapper/microsoftstore/store_windows.go b/storeapi/go-wrapper/microsoftstore/store_windows.go index c30fa0f11..7d98388e1 100644 --- a/storeapi/go-wrapper/microsoftstore/store_windows.go +++ b/storeapi/go-wrapper/microsoftstore/store_windows.go @@ -15,7 +15,7 @@ import ( const ( // TODO: Replace with real product ID. - productID = "ABCDEFG" + productID = "9P25B50XMKXT" ) var ( From 8d712327878fececa52b8ad3efd6a3f1dd732d8f Mon Sep 17 00:00:00 2001 From: Carlos Date: Wed, 4 Oct 2023 21:47:19 -0300 Subject: [PATCH 05/16] Using build tag to determine proURL In production we just parse the defaultProURL constant. If the build tag `server_mocks` is active, then we read the environment variable `UP4W_CONTRACTS_BACKEND_MOCK_ENDPOINT`. Mocked MS Store will never run with a production CS backend. Wouldn't make sense. CS backend talks to the real store. Thus we use the presence of the env var UP4W_TEST_WITH_MS_STORE_MOCK to activate the build tag. --- msix/agent/agent.targets | 3 ++- windows-agent/internal/contracts/contracts.go | 6 ++---- .../internal/contracts/default_url.go | 11 +++++++++++ .../internal/contracts/default_url_mock.go | 18 ++++++++++++++++++ 4 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 windows-agent/internal/contracts/default_url.go create mode 100644 windows-agent/internal/contracts/default_url_mock.go diff --git a/msix/agent/agent.targets b/msix/agent/agent.targets index 3a38588c7..f4f6c64bd 100644 --- a/msix/agent/agent.targets +++ b/msix/agent/agent.targets @@ -6,6 +6,7 @@ -ldflags -H=windowsgui + -tags=server_mocks @@ -32,7 +33,7 @@ - + diff --git a/windows-agent/internal/contracts/contracts.go b/windows-agent/internal/contracts/contracts.go index 85e27162c..aa7794bd3 100644 --- a/windows-agent/internal/contracts/contracts.go +++ b/windows-agent/internal/contracts/contracts.go @@ -13,8 +13,6 @@ import ( "github.com/ubuntu/decorate" ) -const defaultProURL = "https://contracts.canonical.com" - type options struct { proURL *url.URL microsoftStore MicrosoftStore @@ -67,9 +65,9 @@ func ProToken(ctx context.Context, args ...Option) (token string, err error) { } if opts.proURL == nil { - url, err := url.Parse(defaultProURL) + url, err := defaultProBackendURL() if err != nil { - return "", fmt.Errorf("could not parse default contract server URL %q: %v", defaultProURL, err) + return "", fmt.Errorf("could not parse default contract server URL: %v", err) } opts.proURL = url } diff --git a/windows-agent/internal/contracts/default_url.go b/windows-agent/internal/contracts/default_url.go new file mode 100644 index 000000000..f0e44dae2 --- /dev/null +++ b/windows-agent/internal/contracts/default_url.go @@ -0,0 +1,11 @@ +//go:build !server_mocks + +package contracts + +import "net/url" + +const defaultProURL = "https://contracts.canonical.com" + +func defaultProBackendURL() (*url.URL, error) { + return url.Parse(defaultProURL) +} diff --git a/windows-agent/internal/contracts/default_url_mock.go b/windows-agent/internal/contracts/default_url_mock.go new file mode 100644 index 000000000..7908179d4 --- /dev/null +++ b/windows-agent/internal/contracts/default_url_mock.go @@ -0,0 +1,18 @@ +//go:build server_mocks + +package contracts + +import ( + "errors" + "fmt" + "net/url" + "os" +) + +func defaultProBackendURL() (*url.URL, error) { + endpoint := os.Getenv("UP4W_CONTRACTS_BACKEND_MOCK_ENDPOINT") + if len(endpoint) == 0 { + return nil, errors.New("Cannot read contracts backend mock endpoint from environment. Please set UP4W_CONTRACTS_BACKEND_MOCK_ENDPOINT.") + } + return url.Parse(fmt.Sprintf("http://%s", endpoint)) +} From 0efff47299349f3068209a12c1f34bd79f8c7957 Mon Sep 17 00:00:00 2001 From: Carlos Date: Wed, 4 Oct 2023 22:13:39 -0300 Subject: [PATCH 06/16] GUI roleplay for the purchase end-to-end test Starts the agent; waits for its communication; taps on the "Subscribe Now" button; waits the agent's response; transitions to the store-managed status page; and quits. --- .../ubuntupro/end_to_end/end_to_end_test.dart | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/gui/packages/ubuntupro/end_to_end/end_to_end_test.dart b/gui/packages/ubuntupro/end_to_end/end_to_end_test.dart index e6feda9ec..9aa5f17d1 100644 --- a/gui/packages/ubuntupro/end_to_end/end_to_end_test.dart +++ b/gui/packages/ubuntupro/end_to_end/end_to_end_test.dart @@ -26,6 +26,7 @@ void main(List args) { const testCases = { 'TestOrganizationProvidedToken': testOrganizationProvidedToken, 'TestManualTokenInput': testManualTokenInput, + 'TestPurchase': testPurchase, }; final scenario = args[0]; @@ -87,3 +88,20 @@ Future testManualTokenInput(WidgetTester tester) async { l10n = tester.l10n(); expect(find.text(l10n.manuallyManaged), findsOneWidget); } + +Future 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(); + 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(); + expect(find.text(l10n.storeManaged), findsOneWidget); +} From 8440bfc1cc060b6d0aeff3e9ce72c3500e000b13 Mon Sep 17 00:00:00 2001 From: Carlos Date: Fri, 6 Oct 2023 14:41:46 -0300 Subject: [PATCH 07/16] Implements the TestPurchase scenario in Go --- end-to-end/purchase_test.go | 150 ++++++++++++++++++ .../TestPurchase/storemock_config.yaml | 49 ++++++ 2 files changed, 199 insertions(+) create mode 100644 end-to-end/purchase_test.go create mode 100644 end-to-end/testdata/TestPurchase/storemock_config.yaml diff --git a/end-to-end/purchase_test.go b/end-to-end/purchase_test.go new file mode 100644 index 000000000..5a257db43 --- /dev/null +++ b/end-to-end/purchase_test.go @@ -0,0 +1,150 @@ +package endtoend_test + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + "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" + "golang.org/x/exp/slog" + "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) + + ctx := context.Background() + contractsCtx, contractsCancel := context.WithCancel(ctx) + defer contractsCancel() + + settings := contractsmockserver.DefaultSettings() + + if len(tc.withToken) > 0 { + settings.Subscription.OnSuccess.Value = tc.withToken + } else { + token := os.Getenv(proTokenEnv) + if len(token) == 0 { + slog.Error("UP4W_TEST_PRO_TOKEN environment variable must be set to a valid Pro Token") + os.Exit(1) + } + settings.Subscription.OnSuccess.Value = token + } + cs := contractsmockserver.NewServer(settings) + 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()) + //nolint:errcheck // Nothing we can do about it + defer cs.Stop() + storeCtx, storeCancel := context.WithCancel(ctx) + defer storeCancel() + + storeSettings := storemockserver.DefaultSettings() + + testData, err := os.ReadFile("testdata/TestPurchase/storemock_config.yaml") + if err != nil { + slog.Error(fmt.Sprintf("Could not read input file: %v", err)) + os.Exit(1) + } + + yaml.Unmarshal(testData, &storeSettings) + + store := storemockserver.NewServer(storeSettings) + 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()) + //nolint:errcheck // Nothing we can do about it + defer store.Stop() + + // 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") + }) + } +} diff --git a/end-to-end/testdata/TestPurchase/storemock_config.yaml b/end-to-end/testdata/TestPurchase/storemock_config.yaml new file mode 100644 index 000000000..1a3dc5f3e --- /dev/null +++ b/end-to-end/testdata/TestPurchase/storemock_config.yaml @@ -0,0 +1,49 @@ +generateuserjwt: + onsuccess: + value: CPP_MOCK_JWT + status: 200 + disabled: false + blocked: false +getproducts: + onsuccess: + value: "" + status: 200 + disabled: false + blocked: false +purchase: + onsuccess: + value: "" + status: 200 + disabled: false + blocked: false +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 From c533e8f854cfa86f88ae74803313b144dfaf7c77 Mon Sep 17 00:00:00 2001 From: Carlos Date: Wed, 18 Oct 2023 11:35:29 -0300 Subject: [PATCH 08/16] Check error when unmarshalling test data --- end-to-end/purchase_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/end-to-end/purchase_test.go b/end-to-end/purchase_test.go index 5a257db43..f52d14c00 100644 --- a/end-to-end/purchase_test.go +++ b/end-to-end/purchase_test.go @@ -87,7 +87,8 @@ func TestPurchase(t *testing.T) { os.Exit(1) } - yaml.Unmarshal(testData, &storeSettings) + err = yaml.Unmarshal(testData, &storeSettings) + require.NoError(t, err, "Setup: Unmarshalling test data should return no error") store := storemockserver.NewServer(storeSettings) if !tc.storeDown { From e9b7c0e3c1efa50fc87f5e783cc228a3b83fb2fb Mon Sep 17 00:00:00 2001 From: Carlos Date: Wed, 18 Oct 2023 12:15:18 -0300 Subject: [PATCH 09/16] Update go module --- end-to-end/go.mod | 12 ++++++++---- end-to-end/go.sum | 19 ++++++++++++++++--- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/end-to-end/go.mod b/end-to-end/go.mod index 4f57baf46..52c0d47af 100644 --- a/end-to-end/go.mod +++ b/end-to-end/go.mod @@ -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/exp v0.0.0-20231006140011-7918f672742d 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 + gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/end-to-end/go.sum b/end-to-end/go.sum index d6efa5e28..106d55cfc 100644 --- a/end-to-end/go.sum +++ b/end-to-end/go.sum @@ -2,15 +2,19 @@ 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= @@ -18,8 +22,13 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA 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= @@ -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= From f4146fda70cef4e301632398f440e3ee3df3930d Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 19 Oct 2023 10:45:08 -0300 Subject: [PATCH 10/16] Let the mutex protect the entire loop We could race with simultaneous requests. --- mocks/storeserver/storemockserver/storemockserver.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mocks/storeserver/storemockserver/storemockserver.go b/mocks/storeserver/storemockserver/storemockserver.go index d65538bdf..3705e4000 100644 --- a/mocks/storeserver/storemockserver/storemockserver.go +++ b/mocks/storeserver/storemockserver/storemockserver.go @@ -273,6 +273,9 @@ func (s *Server) handlePurchase(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") + s.settingsMu.Lock() + defer s.settingsMu.Unlock() + for i, p := range s.settings.AllProducts { if p.StoreID != id { continue @@ -286,8 +289,6 @@ func (s *Server) handlePurchase(w http.ResponseWriter, r *http.Request) { year, month, day := time.Now().Date() - s.settingsMu.Lock() - defer s.settingsMu.Unlock() s.settings.AllProducts[i].ExpirationDate = time.Date(year+1, month, day, 1, 1, 1, 1, time.Local) // one year from now. s.settings.AllProducts[i].IsInUserCollection = true fmt.Fprintf(w, `{%q:%q}`, PurchaseStatusKey, SucceededResult) From 2cc8ad2ba0020e75ff3af8cc59cec8fa507de69b Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 19 Oct 2023 10:57:52 -0300 Subject: [PATCH 11/16] Avoids if-else blocks --- end-to-end/purchase_test.go | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/end-to-end/purchase_test.go b/end-to-end/purchase_test.go index f52d14c00..98e356a7c 100644 --- a/end-to-end/purchase_test.go +++ b/end-to-end/purchase_test.go @@ -58,16 +58,13 @@ func TestPurchase(t *testing.T) { settings := contractsmockserver.DefaultSettings() - if len(tc.withToken) > 0 { - settings.Subscription.OnSuccess.Value = tc.withToken - } else { - token := os.Getenv(proTokenEnv) - if len(token) == 0 { - slog.Error("UP4W_TEST_PRO_TOKEN environment variable must be set to a valid Pro Token") - os.Exit(1) - } - settings.Subscription.OnSuccess.Value = token + token := os.Getenv(proTokenEnv) + if tc.withToken != "" { + token = tc.withToken } + require.NotEmpty(t, token, "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) if !tc.csServerDown { err := cs.Serve(contractsCtx, "localhost:0") From 27567f8811af15e3dc200b72084f6a436534c055 Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 19 Oct 2023 11:06:02 -0300 Subject: [PATCH 12/16] Use the golden package to find the test data --- end-to-end/purchase_test.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/end-to-end/purchase_test.go b/end-to-end/purchase_test.go index 98e356a7c..4639b1517 100644 --- a/end-to-end/purchase_test.go +++ b/end-to-end/purchase_test.go @@ -4,14 +4,15 @@ 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" - "golang.org/x/exp/slog" "gopkg.in/yaml.v3" ) @@ -78,11 +79,8 @@ func TestPurchase(t *testing.T) { storeSettings := storemockserver.DefaultSettings() - testData, err := os.ReadFile("testdata/TestPurchase/storemock_config.yaml") - if err != nil { - slog.Error(fmt.Sprintf("Could not read input file: %v", err)) - os.Exit(1) - } + testData, err := os.ReadFile(filepath.Join(golden.TestFamilyPath(t), "storemock_config.yaml")) + require.NoError(t, err, "Setup: Could not read test fixture input file") err = yaml.Unmarshal(testData, &storeSettings) require.NoError(t, err, "Setup: Unmarshalling test data should return no error") From 180ea2b009928b08b95543cf178839fd39bf5cc6 Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 19 Oct 2023 11:13:10 -0300 Subject: [PATCH 13/16] Reorganizes defer's and context's creations Making those lines more idiomatic `defer s.Stop()` close to the construction of s `ctx := context...` closer to the point of use. --- end-to-end/go.mod | 2 +- end-to-end/purchase_test.go | 28 ++++++++++++++++------------ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/end-to-end/go.mod b/end-to-end/go.mod index 52c0d47af..d66519038 100644 --- a/end-to-end/go.mod +++ b/end-to-end/go.mod @@ -8,7 +8,6 @@ require ( 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/exp v0.0.0-20231006140011-7918f672742d golang.org/x/sys v0.13.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -22,5 +21,6 @@ require ( github.com/sirupsen/logrus v1.9.3 // 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 ) diff --git a/end-to-end/purchase_test.go b/end-to-end/purchase_test.go index 4639b1517..4f22dd8ee 100644 --- a/end-to-end/purchase_test.go +++ b/end-to-end/purchase_test.go @@ -53,10 +53,6 @@ func TestPurchase(t *testing.T) { t.Run(name, func(t *testing.T) { testSetup(t) - ctx := context.Background() - contractsCtx, contractsCancel := context.WithCancel(ctx) - defer contractsCancel() - settings := contractsmockserver.DefaultSettings() token := os.Getenv(proTokenEnv) @@ -67,32 +63,40 @@ func TestPurchase(t *testing.T) { 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()) - //nolint:errcheck // Nothing we can do about it - defer cs.Stop() - storeCtx, storeCancel := context.WithCancel(ctx) - defer storeCancel() - storeSettings := storemockserver.DefaultSettings() + 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()) - //nolint:errcheck // Nothing we can do about it - defer store.Stop() // Either runs the ubuntupro app before... if tc.whenToStartAgent == beforeDistroRegistration { From afee95959b3bd0b7225f35d7fb0afc081175e2bb Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 19 Oct 2023 13:59:14 -0300 Subject: [PATCH 14/16] Keep only the non-default values... ... in the test data storemock_config.yaml --- .../testdata/TestPurchase/storemock_config.yaml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/end-to-end/testdata/TestPurchase/storemock_config.yaml b/end-to-end/testdata/TestPurchase/storemock_config.yaml index 1a3dc5f3e..75968211f 100644 --- a/end-to-end/testdata/TestPurchase/storemock_config.yaml +++ b/end-to-end/testdata/TestPurchase/storemock_config.yaml @@ -2,20 +2,6 @@ generateuserjwt: onsuccess: value: CPP_MOCK_JWT status: 200 - disabled: false - blocked: false -getproducts: - onsuccess: - value: "" - status: 200 - disabled: false - blocked: false -purchase: - onsuccess: - value: "" - status: 200 - disabled: false - blocked: false allproducts: - storeid: 9P25B50XMKXT title: Annual Subscription (production) From 9c11c85ac5da67868201eebd4b6851f6acf7f0fd Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 19 Oct 2023 14:01:15 -0300 Subject: [PATCH 15/16] Enforce end-to-end tests to build with mocks... ... if using the .\tools\build\build-appx.ps1 script. --- tools/build/build-appx.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/build/build-appx.ps1 b/tools/build/build-appx.ps1 index e9a099c2d..bd21205c8 100644 --- a/tools/build/build-appx.ps1 +++ b/tools/build/build-appx.ps1 @@ -101,8 +101,8 @@ catch { Start-VsDevShell } -If ($mode -eq 'production' -and $null -ne $env:UP4W_TEST_WITH_MS_STORE_MOCK) { - Write-Warning "Building the app in Release mode with UP4W_TEST_WITH_MS_STORE_MOCK env var set may lead to build failure or surprising results. Value is $env:UP4W_TEST_WITH_MS_STORE_MOCK." +If ($mode -eq 'end_to_end_tests') { + $env:UP4W_TEST_WITH_MS_STORE_MOCK = 1 } msbuild.exe ` From cd08f086db60ebeeb797a778b5f65d3ed33a6ffc Mon Sep 17 00:00:00 2001 From: Carlos Date: Fri, 20 Oct 2023 09:30:37 -0300 Subject: [PATCH 16/16] Fix setup error message on required pro token --- end-to-end/purchase_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/end-to-end/purchase_test.go b/end-to-end/purchase_test.go index 4f22dd8ee..0efadc1fb 100644 --- a/end-to-end/purchase_test.go +++ b/end-to-end/purchase_test.go @@ -59,7 +59,7 @@ func TestPurchase(t *testing.T) { if tc.withToken != "" { token = tc.withToken } - require.NotEmpty(t, token, "Provide a pro token either via UP4W_TEST_PRO_TOKEN environment variable or the test case struct withToken field") + 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)