go.temporal.io/server@v1.23.0/common/archiver/gcloud/visibility_archiver_test.go (about)

     1  // The MIT License
     2  //
     3  // Copyright (c) 2020 Temporal Technologies Inc.  All rights reserved.
     4  //
     5  // Copyright (c) 2020 Uber Technologies, Inc.
     6  //
     7  // Permission is hereby granted, free of charge, to any person obtaining a copy
     8  // of this software and associated documentation files (the "Software"), to deal
     9  // in the Software without restriction, including without limitation the rights
    10  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    11  // copies of the Software, and to permit persons to whom the Software is
    12  // furnished to do so, subject to the following conditions:
    13  //
    14  // The above copyright notice and this permission notice shall be included in
    15  // all copies or substantial portions of the Software.
    16  //
    17  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    18  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    19  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    20  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    21  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    22  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    23  // THE SOFTWARE.
    24  
    25  package gcloud
    26  
    27  import (
    28  	"context"
    29  	"errors"
    30  	"testing"
    31  	"time"
    32  
    33  	"github.com/golang/mock/gomock"
    34  	"github.com/stretchr/testify/require"
    35  	"github.com/stretchr/testify/suite"
    36  	enumspb "go.temporal.io/api/enums/v1"
    37  	"go.temporal.io/api/serviceerror"
    38  	"go.temporal.io/api/workflow/v1"
    39  
    40  	"go.temporal.io/server/common/searchattribute"
    41  	"go.temporal.io/server/common/testing/protorequire"
    42  
    43  	archiverspb "go.temporal.io/server/api/archiver/v1"
    44  	"go.temporal.io/server/common/archiver"
    45  	"go.temporal.io/server/common/archiver/gcloud/connector"
    46  	"go.temporal.io/server/common/convert"
    47  	"go.temporal.io/server/common/log"
    48  	"go.temporal.io/server/common/metrics"
    49  	"go.temporal.io/server/common/primitives/timestamp"
    50  )
    51  
    52  const (
    53  	testWorkflowTypeName     = "test-workflow-type"
    54  	exampleVisibilityRecord  = `{"namespaceId":"test-namespace-id","namespace":"test-namespace","workflowId":"test-workflow-id","runId":"test-run-id","workflowTypeName":"test-workflow-type","startTime":"2020-02-05T09:56:14.804475Z","closeTime":"2020-02-05T09:56:15.946478Z","status":"Completed","historyLength":36,"memo":null,"searchAttributes":null,"historyArchivalUri":"gs://my-bucket-cad/temporal_archival/development"}`
    55  	exampleVisibilityRecord2 = `{"namespaceId":"test-namespace-id","namespace":"test-namespace",
    56  "workflowId":"test-workflow-id2","runId":"test-run-id","workflowTypeName":"test-workflow-type",
    57  "startTime":"2020-02-05T09:56:14.804475Z","closeTime":"2020-02-05T09:56:15.946478Z","status":"Completed","historyLength":36,"memo":null,"searchAttributes":null,"historyArchivalUri":"gs://my-bucket-cad/temporal_archival/development"}`
    58  )
    59  
    60  func (s *visibilityArchiverSuite) SetupTest() {
    61  	s.Assertions = require.New(s.T())
    62  	s.controller = gomock.NewController(s.T())
    63  	s.container = &archiver.VisibilityBootstrapContainer{
    64  		Logger:         log.NewNoopLogger(),
    65  		MetricsHandler: metrics.NoopMetricsHandler,
    66  	}
    67  	s.expectedVisibilityRecords = []*archiverspb.VisibilityRecord{
    68  		{
    69  			NamespaceId:      testNamespaceID,
    70  			Namespace:        testNamespace,
    71  			WorkflowId:       testWorkflowID,
    72  			RunId:            testRunID,
    73  			WorkflowTypeName: testWorkflowTypeName,
    74  			StartTime:        timestamp.UnixOrZeroTimePtr(1580896574804475000),
    75  			CloseTime:        timestamp.UnixOrZeroTimePtr(1580896575946478000),
    76  			Status:           enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED,
    77  			HistoryLength:    36,
    78  		},
    79  	}
    80  }
    81  
    82  func (s *visibilityArchiverSuite) TearDownTest() {
    83  	s.controller.Finish()
    84  }
    85  
    86  func TestVisibilityArchiverSuiteSuite(t *testing.T) {
    87  	suite.Run(t, new(visibilityArchiverSuite))
    88  }
    89  
    90  type visibilityArchiverSuite struct {
    91  	*require.Assertions
    92  	protorequire.ProtoAssertions
    93  	suite.Suite
    94  	controller                *gomock.Controller
    95  	container                 *archiver.VisibilityBootstrapContainer
    96  	expectedVisibilityRecords []*archiverspb.VisibilityRecord
    97  }
    98  
    99  func (s *visibilityArchiverSuite) TestValidateVisibilityURI() {
   100  	testCases := []struct {
   101  		URI         string
   102  		expectedErr error
   103  	}{
   104  		{
   105  			URI:         "wrongscheme:///a/b/c",
   106  			expectedErr: archiver.ErrURISchemeMismatch,
   107  		},
   108  		{
   109  			URI:         "gs:my-bucket-cad/temporal_archival/visibility",
   110  			expectedErr: archiver.ErrInvalidURI,
   111  		},
   112  		{
   113  			URI:         "gs://",
   114  			expectedErr: archiver.ErrInvalidURI,
   115  		},
   116  		{
   117  			URI:         "gs://my-bucket-cad",
   118  			expectedErr: archiver.ErrInvalidURI,
   119  		},
   120  		{
   121  			URI:         "gs:/my-bucket-cad/temporal_archival/visibility",
   122  			expectedErr: archiver.ErrInvalidURI,
   123  		},
   124  		{
   125  			URI:         "gs://my-bucket-cad/temporal_archival/visibility",
   126  			expectedErr: nil,
   127  		},
   128  	}
   129  
   130  	storageWrapper := connector.NewMockClient(s.controller)
   131  	storageWrapper.EXPECT().Exist(gomock.Any(), gomock.Any(), "").Return(false, nil)
   132  	visibilityArchiver := new(visibilityArchiver)
   133  	visibilityArchiver.gcloudStorage = storageWrapper
   134  	for _, tc := range testCases {
   135  		URI, err := archiver.NewURI(tc.URI)
   136  		s.NoError(err)
   137  		s.Equal(tc.expectedErr, visibilityArchiver.ValidateURI(URI))
   138  	}
   139  }
   140  
   141  func (s *visibilityArchiverSuite) TestArchive_Fail_InvalidVisibilityURI() {
   142  	ctx := context.Background()
   143  	URI, err := archiver.NewURI("wrongscheme://")
   144  	s.NoError(err)
   145  	storageWrapper := connector.NewMockClient(s.controller)
   146  
   147  	visibilityArchiver := newVisibilityArchiver(s.container, storageWrapper)
   148  	s.NoError(err)
   149  	request := &archiverspb.VisibilityRecord{
   150  		NamespaceId: testNamespaceID,
   151  		Namespace:   testNamespace,
   152  		WorkflowId:  testWorkflowID,
   153  		RunId:       testRunID,
   154  	}
   155  
   156  	err = visibilityArchiver.Archive(ctx, URI, request)
   157  	s.Error(err)
   158  }
   159  
   160  func (s *visibilityArchiverSuite) TestQuery_Fail_InvalidVisibilityURI() {
   161  	ctx := context.Background()
   162  	URI, err := archiver.NewURI("wrongscheme://")
   163  	s.NoError(err)
   164  	storageWrapper := connector.NewMockClient(s.controller)
   165  
   166  	visibilityArchiver := newVisibilityArchiver(s.container, storageWrapper)
   167  	s.NoError(err)
   168  	request := &archiver.QueryVisibilityRequest{
   169  		NamespaceID: testNamespaceID,
   170  		PageSize:    10,
   171  		Query:       "WorkflowType='type::example' AND CloseTime='2020-02-05T11:00:00Z' AND SearchPrecision='Day'",
   172  	}
   173  
   174  	_, err = visibilityArchiver.Query(ctx, URI, request, searchattribute.TestNameTypeMap)
   175  	s.Error(err)
   176  }
   177  
   178  func (s *visibilityArchiverSuite) TestVisibilityArchive() {
   179  	ctx := context.Background()
   180  	URI, err := archiver.NewURI("gs://my-bucket-cad/temporal_archival/visibility")
   181  	s.NoError(err)
   182  	storageWrapper := connector.NewMockClient(s.controller)
   183  	storageWrapper.EXPECT().Exist(gomock.Any(), URI, gomock.Any()).Return(false, nil)
   184  	storageWrapper.EXPECT().Upload(gomock.Any(), URI, gomock.Any(), gomock.Any()).Return(nil).Times(2)
   185  
   186  	visibilityArchiver := newVisibilityArchiver(s.container, storageWrapper)
   187  	s.NoError(err)
   188  
   189  	request := &archiverspb.VisibilityRecord{
   190  		Namespace:        testNamespace,
   191  		NamespaceId:      testNamespaceID,
   192  		WorkflowId:       testWorkflowID,
   193  		RunId:            testRunID,
   194  		WorkflowTypeName: testWorkflowTypeName,
   195  		StartTime:        timestamp.TimeNowPtrUtc(),
   196  		ExecutionTime:    nil, // workflow without backoff
   197  		CloseTime:        timestamp.TimeNowPtrUtc(),
   198  		Status:           enumspb.WORKFLOW_EXECUTION_STATUS_FAILED,
   199  		HistoryLength:    int64(101),
   200  	}
   201  
   202  	err = visibilityArchiver.Archive(ctx, URI, request)
   203  	s.NoError(err)
   204  }
   205  
   206  func (s *visibilityArchiverSuite) TestQuery_Fail_InvalidQuery() {
   207  	ctx := context.Background()
   208  	URI, err := archiver.NewURI("gs://my-bucket-cad/temporal_archival/visibility")
   209  	s.NoError(err)
   210  	storageWrapper := connector.NewMockClient(s.controller)
   211  	storageWrapper.EXPECT().Exist(gomock.Any(), URI, gomock.Any()).Return(false, nil)
   212  	visibilityArchiver := newVisibilityArchiver(s.container, storageWrapper)
   213  	s.NoError(err)
   214  
   215  	mockParser := NewMockQueryParser(s.controller)
   216  	mockParser.EXPECT().Parse(gomock.Any()).Return(nil, errors.New("invalid query"))
   217  	visibilityArchiver.queryParser = mockParser
   218  	response, err := visibilityArchiver.Query(ctx, URI, &archiver.QueryVisibilityRequest{
   219  		NamespaceID: "some random namespaceID",
   220  		PageSize:    10,
   221  		Query:       "some invalid query",
   222  	}, searchattribute.TestNameTypeMap)
   223  	s.Error(err)
   224  	s.Nil(response)
   225  }
   226  
   227  func (s *visibilityArchiverSuite) TestQuery_Fail_InvalidToken() {
   228  	URI, err := archiver.NewURI("gs://my-bucket-cad/temporal_archival/visibility")
   229  	s.NoError(err)
   230  	storageWrapper := connector.NewMockClient(s.controller)
   231  	storageWrapper.EXPECT().Exist(gomock.Any(), URI, gomock.Any()).Return(false, nil)
   232  	visibilityArchiver := newVisibilityArchiver(s.container, storageWrapper)
   233  	s.NoError(err)
   234  
   235  	mockParser := NewMockQueryParser(s.controller)
   236  	startTime, _ := time.Parse(time.RFC3339, "2019-10-04T11:00:00+00:00")
   237  	closeTime := startTime.Add(time.Hour)
   238  	precision := PrecisionDay
   239  	mockParser.EXPECT().Parse(gomock.Any()).Return(&parsedQuery{
   240  		closeTime:       closeTime,
   241  		startTime:       startTime,
   242  		searchPrecision: &precision,
   243  	}, nil)
   244  	visibilityArchiver.queryParser = mockParser
   245  	request := &archiver.QueryVisibilityRequest{
   246  		NamespaceID:   testNamespaceID,
   247  		Query:         "parsed by mockParser",
   248  		PageSize:      1,
   249  		NextPageToken: []byte{1, 2, 3},
   250  	}
   251  	response, err := visibilityArchiver.Query(context.Background(), URI, request, searchattribute.TestNameTypeMap)
   252  	s.Error(err)
   253  	s.Nil(response)
   254  }
   255  
   256  func (s *visibilityArchiverSuite) TestQuery_Success_NoNextPageToken() {
   257  	ctx := context.Background()
   258  	URI, err := archiver.NewURI("gs://my-bucket-cad/temporal_archival/visibility")
   259  	s.NoError(err)
   260  	storageWrapper := connector.NewMockClient(s.controller)
   261  	storageWrapper.EXPECT().Exist(gomock.Any(), URI, gomock.Any()).Return(false, nil)
   262  	storageWrapper.EXPECT().QueryWithFilters(gomock.Any(), URI, gomock.Any(), 10, 0, gomock.Any()).Return([]string{"closeTimeout_2020-02-05T09:56:14Z_test-workflow-id_MobileOnlyWorkflow::processMobileOnly_test-run-id.visibility"}, true, 1, nil)
   263  	storageWrapper.EXPECT().Get(gomock.Any(), URI, "test-namespace-id/closeTimeout_2020-02-05T09:56:14Z_test-workflow-id_MobileOnlyWorkflow::processMobileOnly_test-run-id.visibility").Return([]byte(exampleVisibilityRecord), nil)
   264  
   265  	visibilityArchiver := newVisibilityArchiver(s.container, storageWrapper)
   266  	s.NoError(err)
   267  
   268  	mockParser := NewMockQueryParser(s.controller)
   269  	dayPrecision := "Day"
   270  	closeTime, _ := time.Parse(time.RFC3339, "2019-10-04T11:00:00+00:00")
   271  	mockParser.EXPECT().Parse(gomock.Any()).Return(&parsedQuery{
   272  		closeTime:       closeTime,
   273  		searchPrecision: &dayPrecision,
   274  		workflowType:    convert.StringPtr("MobileOnlyWorkflow::processMobileOnly"),
   275  		workflowID:      convert.StringPtr(testWorkflowID),
   276  		runID:           convert.StringPtr(testRunID),
   277  	}, nil)
   278  	visibilityArchiver.queryParser = mockParser
   279  	request := &archiver.QueryVisibilityRequest{
   280  		NamespaceID: testNamespaceID,
   281  		PageSize:    10,
   282  		Query:       "parsed by mockParser",
   283  	}
   284  
   285  	response, err := visibilityArchiver.Query(ctx, URI, request, searchattribute.TestNameTypeMap)
   286  	s.NoError(err)
   287  	s.NotNil(response)
   288  	s.Nil(response.NextPageToken)
   289  	s.Len(response.Executions, 1)
   290  	ei, err := convertToExecutionInfo(s.expectedVisibilityRecords[0], searchattribute.TestNameTypeMap)
   291  	s.NoError(err)
   292  	s.ProtoEqual(ei, response.Executions[0])
   293  }
   294  
   295  func (s *visibilityArchiverSuite) TestQuery_Success_SmallPageSize() {
   296  	pageSize := 2
   297  	ctx := context.Background()
   298  	URI, err := archiver.NewURI("gs://my-bucket-cad/temporal_archival/visibility")
   299  	s.NoError(err)
   300  	storageWrapper := connector.NewMockClient(s.controller)
   301  	storageWrapper.EXPECT().Exist(gomock.Any(), URI, gomock.Any()).Return(false, nil).Times(2)
   302  	storageWrapper.EXPECT().QueryWithFilters(gomock.Any(), URI, gomock.Any(), pageSize, 0, gomock.Any()).Return([]string{"closeTimeout_2020-02-05T09:56:14Z_test-workflow-id_MobileOnlyWorkflow::processMobileOnly_test-run-id.visibility", "closeTimeout_2020-02-05T09:56:15Z_test-workflow-id_MobileOnlyWorkflow::processMobileOnly_test-run-id.visibility"}, false, 1, nil)
   303  	storageWrapper.EXPECT().QueryWithFilters(gomock.Any(), URI, gomock.Any(), pageSize, 1, gomock.Any()).Return([]string{"closeTimeout_2020-02-05T09:56:16Z_test-workflow-id_MobileOnlyWorkflow::processMobileOnly_test-run-id.visibility"}, true, 2, nil)
   304  	storageWrapper.EXPECT().Get(gomock.Any(), URI, "test-namespace-id/closeTimeout_2020-02-05T09:56:14Z_test-workflow-id_MobileOnlyWorkflow::processMobileOnly_test-run-id.visibility").Return([]byte(exampleVisibilityRecord), nil)
   305  	storageWrapper.EXPECT().Get(gomock.Any(), URI, "test-namespace-id/closeTimeout_2020-02-05T09:56:15Z_test-workflow-id_MobileOnlyWorkflow::processMobileOnly_test-run-id.visibility").Return([]byte(exampleVisibilityRecord), nil)
   306  	storageWrapper.EXPECT().Get(gomock.Any(), URI, "test-namespace-id/closeTimeout_2020-02-05T09:56:16Z_test-workflow-id_MobileOnlyWorkflow::processMobileOnly_test-run-id.visibility").Return([]byte(exampleVisibilityRecord), nil)
   307  
   308  	visibilityArchiver := newVisibilityArchiver(s.container, storageWrapper)
   309  	s.NoError(err)
   310  
   311  	mockParser := NewMockQueryParser(s.controller)
   312  	dayPrecision := "Day"
   313  	closeTime, _ := time.Parse(time.RFC3339, "2019-10-04T11:00:00+00:00")
   314  	mockParser.EXPECT().Parse(gomock.Any()).Return(&parsedQuery{
   315  		closeTime:       closeTime,
   316  		searchPrecision: &dayPrecision,
   317  		workflowType:    convert.StringPtr("MobileOnlyWorkflow::processMobileOnly"),
   318  		workflowID:      convert.StringPtr(testWorkflowID),
   319  		runID:           convert.StringPtr(testRunID),
   320  	}, nil).AnyTimes()
   321  	visibilityArchiver.queryParser = mockParser
   322  	request := &archiver.QueryVisibilityRequest{
   323  		NamespaceID: testNamespaceID,
   324  		PageSize:    pageSize,
   325  		Query:       "parsed by mockParser",
   326  	}
   327  
   328  	response, err := visibilityArchiver.Query(ctx, URI, request, searchattribute.TestNameTypeMap)
   329  	s.NoError(err)
   330  	s.NotNil(response)
   331  	s.NotNil(response.NextPageToken)
   332  	s.Len(response.Executions, 2)
   333  	ei, err := convertToExecutionInfo(s.expectedVisibilityRecords[0], searchattribute.TestNameTypeMap)
   334  	s.NoError(err)
   335  	s.ProtoEqual(ei, response.Executions[0])
   336  	ei, err = convertToExecutionInfo(s.expectedVisibilityRecords[0], searchattribute.TestNameTypeMap)
   337  	s.NoError(err)
   338  	s.ProtoEqual(ei, response.Executions[1])
   339  
   340  	request.NextPageToken = response.NextPageToken
   341  	response, err = visibilityArchiver.Query(ctx, URI, request, searchattribute.TestNameTypeMap)
   342  	s.NoError(err)
   343  	s.NotNil(response)
   344  	s.Nil(response.NextPageToken)
   345  	s.Len(response.Executions, 1)
   346  	ei, err = convertToExecutionInfo(s.expectedVisibilityRecords[0], searchattribute.TestNameTypeMap)
   347  	s.NoError(err)
   348  	s.ProtoEqual(ei, response.Executions[0])
   349  }
   350  
   351  func (s *visibilityArchiverSuite) TestQuery_EmptyQuery_InvalidNamespace() {
   352  	URI, err := archiver.NewURI("gs://my-bucket-cad/temporal_archival/visibility")
   353  	s.NoError(err)
   354  	storageWrapper := connector.NewMockClient(s.controller)
   355  	storageWrapper.EXPECT().Exist(gomock.Any(), URI, gomock.Any()).Return(false, nil)
   356  	arc := newVisibilityArchiver(s.container, storageWrapper)
   357  	req := &archiver.QueryVisibilityRequest{
   358  		NamespaceID:   "",
   359  		PageSize:      1,
   360  		NextPageToken: nil,
   361  		Query:         "",
   362  	}
   363  	_, err = arc.Query(context.Background(), URI, req, searchattribute.TestNameTypeMap)
   364  
   365  	var svcErr *serviceerror.InvalidArgument
   366  
   367  	s.ErrorAs(err, &svcErr)
   368  }
   369  
   370  func (s *visibilityArchiverSuite) TestQuery_EmptyQuery_ZeroPageSize() {
   371  	URI, err := archiver.NewURI("gs://my-bucket-cad/temporal_archival/visibility")
   372  	s.NoError(err)
   373  	storageWrapper := connector.NewMockClient(s.controller)
   374  	storageWrapper.EXPECT().Exist(gomock.Any(), URI, gomock.Any()).Return(false, nil)
   375  	arc := newVisibilityArchiver(s.container, storageWrapper)
   376  
   377  	req := &archiver.QueryVisibilityRequest{
   378  		NamespaceID:   testNamespaceID,
   379  		PageSize:      0,
   380  		NextPageToken: nil,
   381  		Query:         "",
   382  	}
   383  	_, err = arc.Query(context.Background(), URI, req, searchattribute.TestNameTypeMap)
   384  
   385  	var svcErr *serviceerror.InvalidArgument
   386  
   387  	s.ErrorAs(err, &svcErr)
   388  }
   389  
   390  func (s *visibilityArchiverSuite) TestQuery_EmptyQuery_Pagination() {
   391  	URI, err := archiver.NewURI("gs://my-bucket-cad/temporal_archival/visibility")
   392  	s.NoError(err)
   393  	storageWrapper := connector.NewMockClient(s.controller)
   394  	storageWrapper.EXPECT().Exist(gomock.Any(), URI, gomock.Any()).Return(true, nil).Times(2)
   395  	storageWrapper.EXPECT().QueryWithFilters(
   396  		gomock.Any(),
   397  		URI,
   398  		gomock.Any(),
   399  		1,
   400  		0,
   401  		gomock.Any(),
   402  	).Return(
   403  		[]string{"closeTimeout_2020-02-05T09:56:14Z_test-workflow-id_MobileOnlyWorkflow::processMobileOnly_test-run-id.visibility"},
   404  		false,
   405  		1,
   406  		nil,
   407  	)
   408  	storageWrapper.EXPECT().QueryWithFilters(
   409  		gomock.Any(),
   410  		URI,
   411  		gomock.Any(),
   412  		1,
   413  		1,
   414  		gomock.Any(),
   415  	).Return(
   416  		[]string{"closeTimeout_2020-02-05T09:56:14Z_test-workflow-id2_MobileOnlyWorkflow::processMobileOnly_test-run" +
   417  			"-id.visibility"},
   418  		true,
   419  		2,
   420  		nil,
   421  	)
   422  	storageWrapper.EXPECT().Get(
   423  		gomock.Any(),
   424  		URI,
   425  		"test-namespace-id/closeTimeout_2020-02-05T09:56:14Z_test-workflow-id_MobileOnlyWorkflow::processMobileOnly_test-run-id.visibility",
   426  	).Return([]byte(exampleVisibilityRecord), nil)
   427  	storageWrapper.EXPECT().Get(gomock.Any(), URI,
   428  		"test-namespace-id/closeTimeout_2020-02-05T09:56:14Z_test-workflow-id2_MobileOnlyWorkflow"+
   429  			"::processMobileOnly_test-run-id.visibility").Return([]byte(exampleVisibilityRecord2), nil)
   430  
   431  	arc := newVisibilityArchiver(s.container, storageWrapper)
   432  
   433  	response := &archiver.QueryVisibilityResponse{
   434  		Executions:    nil,
   435  		NextPageToken: nil,
   436  	}
   437  
   438  	limit := 10
   439  	executions := make(map[string]*workflow.WorkflowExecutionInfo, limit)
   440  
   441  	numPages := 2
   442  	for i := 0; i < numPages; i++ {
   443  		req := &archiver.QueryVisibilityRequest{
   444  			NamespaceID:   testNamespaceID,
   445  			PageSize:      1,
   446  			NextPageToken: response.NextPageToken,
   447  			Query:         "",
   448  		}
   449  		response, err = arc.Query(context.Background(), URI, req, searchattribute.TestNameTypeMap)
   450  		s.NoError(err)
   451  		s.NotNil(response)
   452  		s.Len(response.Executions, 1)
   453  
   454  		s.Equal(
   455  			i == numPages-1,
   456  			response.NextPageToken == nil,
   457  			"should have no next page token on the last iteration",
   458  		)
   459  
   460  		for _, execution := range response.Executions {
   461  			key := execution.Execution.GetWorkflowId() +
   462  				"/" + execution.Execution.GetRunId() +
   463  				"/" + execution.CloseTime.String()
   464  			executions[key] = execution
   465  		}
   466  	}
   467  	s.Len(executions, 2, "there should be exactly 2 unique executions")
   468  }