Skip to content

Commit af1a053

Browse files
committed
Add adapter logic to handle bookmark operations
This change adds the adapter logic for the putUserSavedSearchBookmark and removeUserSavedSearchBookmark operations. putUserSavedSearchBookmark handles converting the errors from spanner for 1) bookmark limit exceeded and 2) entity does not exist to errors that the http layer will use to return the appropriate status code removeUserSavedSearchBookmark handles converting the errors from spanner for 1) saved search owner cannot delete bookmark and 2) entity does not exist to errors that the http layer will use to return the appropriate status code
1 parent cb66728 commit af1a053

File tree

3 files changed

+210
-0
lines changed

3 files changed

+210
-0
lines changed

lib/gcpspanner/spanneradapters/backend.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ type BackendSpannerClient interface {
125125
pageSize int,
126126
pageToken *string) (*gcpspanner.UserSavedSearchesPage, error)
127127
UpdateUserSavedSearch(ctx context.Context, req gcpspanner.UpdateSavedSearchRequest) error
128+
AddUserSearchBookmark(ctx context.Context, req gcpspanner.UserSavedSearchBookmark) error
129+
DeleteUserSearchBookmark(ctx context.Context, req gcpspanner.UserSavedSearchBookmark) error
128130
}
129131

130132
// Backend converts queries to spanner to usable entities for the backend
@@ -617,6 +619,50 @@ func (s *Backend) UpdateUserSavedSearch(
617619
return convertUserSavedSearchToSavedSearchResponse(savedSearch), nil
618620
}
619621

622+
func (s *Backend) PutUserSavedSearchBookmark(
623+
ctx context.Context,
624+
userID string,
625+
savedSearchID string,
626+
) error {
627+
err := s.client.AddUserSearchBookmark(ctx, gcpspanner.UserSavedSearchBookmark{
628+
UserID: userID,
629+
SavedSearchID: savedSearchID,
630+
})
631+
if err != nil {
632+
if errors.Is(err, gcpspanner.ErrUserSearchBookmarkLimitExceeded) {
633+
return errors.Join(err, backendtypes.ErrUserMaxBookmarks)
634+
} else if errors.Is(err, gcpspanner.ErrQueryReturnedNoResults) {
635+
return errors.Join(err, backendtypes.ErrEntityDoesNotExist)
636+
}
637+
638+
return err
639+
}
640+
641+
return nil
642+
}
643+
644+
func (s *Backend) RemoveUserSavedSearchBookmark(
645+
ctx context.Context,
646+
userID string,
647+
savedSearchID string,
648+
) error {
649+
err := s.client.DeleteUserSearchBookmark(ctx, gcpspanner.UserSavedSearchBookmark{
650+
UserID: userID,
651+
SavedSearchID: savedSearchID,
652+
})
653+
if err != nil {
654+
if errors.Is(err, gcpspanner.ErrOwnerCannotDeleteBookmark) {
655+
return errors.Join(err, backendtypes.ErrUserNotAuthorizedForAction)
656+
} else if errors.Is(err, gcpspanner.ErrQueryReturnedNoResults) {
657+
return errors.Join(err, backendtypes.ErrEntityDoesNotExist)
658+
}
659+
660+
return err
661+
}
662+
663+
return nil
664+
}
665+
620666
func convertUserSavedSearchToSavedSearchResponse(savedSearch *gcpspanner.UserSavedSearch) *backend.SavedSearchResponse {
621667
return &backend.SavedSearchResponse{
622668
Id: savedSearch.ID,

lib/gcpspanner/spanneradapters/backend_test.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,16 @@ type mockUpdateUserSavedSearchConfig struct {
115115
returnedError error
116116
}
117117

118+
type mockAddUserSearchBookmarkConfig struct {
119+
expectedRequest gcpspanner.UserSavedSearchBookmark
120+
returnedError error
121+
}
122+
123+
type mockDeleteUserSearchBookmarkConfig struct {
124+
expectedRequest gcpspanner.UserSavedSearchBookmark
125+
returnedError error
126+
}
127+
118128
type mockBackendSpannerClient struct {
119129
t *testing.T
120130
aggregationData []gcpspanner.WPTRunAggregationMetricWithTime
@@ -132,10 +142,32 @@ type mockBackendSpannerClient struct {
132142
mockDeleteUserSavedSearchCfg *mockDeleteUserSavedSearchConfig
133143
mockListUserSavedSearchesCfg *mockListUserSavedSearchesConfig
134144
mockUpdateUserSavedSearchCfg *mockUpdateUserSavedSearchConfig
145+
mockAddUserSearchBookmarkCfg *mockAddUserSearchBookmarkConfig
146+
mockDeleteUserSearchBookmarkCfg *mockDeleteUserSearchBookmarkConfig
135147
pageToken *string
136148
err error
137149
}
138150

151+
// AddUserSearchBookmark implements BackendSpannerClient.
152+
func (c mockBackendSpannerClient) AddUserSearchBookmark(
153+
_ context.Context, req gcpspanner.UserSavedSearchBookmark) error {
154+
if !reflect.DeepEqual(req, c.mockAddUserSearchBookmarkCfg.expectedRequest) {
155+
c.t.Error("unexpected input to mock")
156+
}
157+
158+
return c.mockAddUserSearchBookmarkCfg.returnedError
159+
}
160+
161+
// DeleteUserSearchBookmark implements BackendSpannerClient.
162+
func (c mockBackendSpannerClient) DeleteUserSearchBookmark(
163+
_ context.Context, req gcpspanner.UserSavedSearchBookmark) error {
164+
if !reflect.DeepEqual(req, c.mockDeleteUserSearchBookmarkCfg.expectedRequest) {
165+
c.t.Error("unexpected input to mock")
166+
}
167+
168+
return c.mockDeleteUserSearchBookmarkCfg.returnedError
169+
}
170+
139171
func (c mockBackendSpannerClient) ListUserSavedSearches(
140172
_ context.Context, userID string, pageSize int, pageToken *string) (*gcpspanner.UserSavedSearchesPage, error) {
141173
if userID != c.mockListUserSavedSearchesCfg.expectedUserID ||
@@ -2401,6 +2433,134 @@ func TestUpdateUserSavedSearch(t *testing.T) {
24012433
}
24022434
}
24032435

2436+
// nolint: dupl // WONTFIX
2437+
func TestAddUserSavedSearchBookmark(t *testing.T) {
2438+
userID := "test-add-user"
2439+
savedSearchID := "test-add-id"
2440+
bookmark := gcpspanner.UserSavedSearchBookmark{
2441+
UserID: "test-add-user",
2442+
SavedSearchID: "test-add-id",
2443+
}
2444+
testCases := []struct {
2445+
name string
2446+
cfg *mockAddUserSearchBookmarkConfig
2447+
expectedErr error
2448+
}{
2449+
{
2450+
name: "success",
2451+
cfg: &mockAddUserSearchBookmarkConfig{
2452+
expectedRequest: bookmark,
2453+
returnedError: nil,
2454+
},
2455+
expectedErr: nil,
2456+
},
2457+
{
2458+
name: "limit exceeded error",
2459+
cfg: &mockAddUserSearchBookmarkConfig{
2460+
expectedRequest: bookmark,
2461+
returnedError: gcpspanner.ErrUserSearchBookmarkLimitExceeded,
2462+
},
2463+
expectedErr: backendtypes.ErrUserMaxBookmarks,
2464+
},
2465+
{
2466+
name: "not found error",
2467+
cfg: &mockAddUserSearchBookmarkConfig{
2468+
expectedRequest: bookmark,
2469+
returnedError: gcpspanner.ErrQueryReturnedNoResults,
2470+
},
2471+
expectedErr: backendtypes.ErrEntityDoesNotExist,
2472+
},
2473+
{
2474+
name: "unknown error",
2475+
cfg: &mockAddUserSearchBookmarkConfig{
2476+
expectedRequest: bookmark,
2477+
returnedError: errTest,
2478+
},
2479+
expectedErr: errTest,
2480+
},
2481+
}
2482+
2483+
for _, tc := range testCases {
2484+
t.Run(tc.name, func(t *testing.T) {
2485+
//nolint: exhaustruct
2486+
mock := mockBackendSpannerClient{
2487+
t: t,
2488+
mockAddUserSearchBookmarkCfg: tc.cfg,
2489+
}
2490+
bk := NewBackend(mock)
2491+
err := bk.PutUserSavedSearchBookmark(context.Background(), userID, savedSearchID)
2492+
2493+
if !errors.Is(err, tc.expectedErr) {
2494+
t.Errorf("unexpected error %s", err)
2495+
}
2496+
})
2497+
}
2498+
}
2499+
2500+
// nolint: dupl // WONTFIX
2501+
func TestRemoveUserSavedSearchBookmark(t *testing.T) {
2502+
userID := "test-remove-user"
2503+
savedSearchID := "test-remove-id"
2504+
bookmark := gcpspanner.UserSavedSearchBookmark{
2505+
UserID: "test-remove-user",
2506+
SavedSearchID: "test-remove-id",
2507+
}
2508+
testCases := []struct {
2509+
name string
2510+
cfg *mockDeleteUserSearchBookmarkConfig
2511+
expectedErr error
2512+
}{
2513+
{
2514+
name: "success",
2515+
cfg: &mockDeleteUserSearchBookmarkConfig{
2516+
expectedRequest: bookmark,
2517+
returnedError: nil,
2518+
},
2519+
expectedErr: nil,
2520+
},
2521+
{
2522+
name: "owner cannot delete bookmark error",
2523+
cfg: &mockDeleteUserSearchBookmarkConfig{
2524+
expectedRequest: bookmark,
2525+
returnedError: gcpspanner.ErrOwnerCannotDeleteBookmark,
2526+
},
2527+
expectedErr: backendtypes.ErrUserNotAuthorizedForAction,
2528+
},
2529+
{
2530+
name: "not found error",
2531+
cfg: &mockDeleteUserSearchBookmarkConfig{
2532+
expectedRequest: bookmark,
2533+
returnedError: gcpspanner.ErrQueryReturnedNoResults,
2534+
},
2535+
expectedErr: backendtypes.ErrEntityDoesNotExist,
2536+
},
2537+
{
2538+
name: "unknown error",
2539+
cfg: &mockDeleteUserSearchBookmarkConfig{
2540+
expectedRequest: bookmark,
2541+
returnedError: errTest,
2542+
},
2543+
expectedErr: errTest,
2544+
},
2545+
}
2546+
2547+
for _, tc := range testCases {
2548+
t.Run(tc.name, func(t *testing.T) {
2549+
//nolint: exhaustruct
2550+
mock := mockBackendSpannerClient{
2551+
t: t,
2552+
mockDeleteUserSearchBookmarkCfg: tc.cfg,
2553+
}
2554+
bk := NewBackend(mock)
2555+
err := bk.RemoveUserSavedSearchBookmark(context.Background(), userID, savedSearchID)
2556+
2557+
if !errors.Is(err, tc.expectedErr) {
2558+
t.Errorf("unexpected error %s", err)
2559+
}
2560+
})
2561+
}
2562+
}
2563+
24042564
func TestBuildUpdateSavedSearchRequestForGCP(t *testing.T) {
24052565
testSavedSearchID := "test-id"
24062566
testUserID := "test-user"

lib/gcpspanner/spanneradapters/backendtypes/types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ var (
2626
// number of allowed saved searches.
2727
ErrUserMaxSavedSearches = errors.New("user has reached the maximum number of allowed saved searches")
2828

29+
// ErrUserMaxBookmarks indicates the user has reached the maximum
30+
// number of allowed bookmarks.
31+
ErrUserMaxBookmarks = errors.New("user has reached the maximum number of allowed bookmarks")
32+
2933
// ErrUserNotAuthorizedForAction indicates the user is not authorized to execute the requested action.
3034
ErrUserNotAuthorizedForAction = errors.New("user not authorized to execute action")
3135

0 commit comments

Comments
 (0)