Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions backend/pkg/httpserver/list_subscriptions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package httpserver

import (
"context"
"log/slog"

"github.com/GoogleChrome/webstatus.dev/lib/gen/openapi/backend"
)

// nolint:ireturn, revive // Expected ireturn for openapi generation.
func (s *Server) ListSubscriptions(
ctx context.Context,
request backend.ListSubscriptionsRequestObject,
) (backend.ListSubscriptionsResponseObject, error) {
userCheck := CheckAuthenticatedUser[backend.ListSubscriptionsResponseObject](ctx, "ListSubscriptions",
func(code int, message string) backend.ListSubscriptionsResponseObject {
return backend.ListSubscriptions500JSONResponse(backend.BasicErrorModel{Code: code, Message: message})
})
if userCheck.User == nil {
return userCheck.Response, nil
}

resp, err := s.wptMetricsStorer.ListSavedSearchSubscriptions(
ctx, userCheck.User.ID, getPageSizeOrDefault(request.Params.PageSize), request.Params.PageToken)
if err != nil {
slog.ErrorContext(ctx, "unable to get page of subscriptions", "error", err)

return backend.ListSubscriptions500JSONResponse{
Code: 500,
Message: "could not list subscriptions",
}, nil
}

return backend.ListSubscriptions200JSONResponse(*resp), nil
}
194 changes: 194 additions & 0 deletions backend/pkg/httpserver/list_subscriptions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package httpserver

import (
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/GoogleChrome/webstatus.dev/lib/auth"
"github.com/GoogleChrome/webstatus.dev/lib/backendtypes"
"github.com/GoogleChrome/webstatus.dev/lib/gen/openapi/backend"
)

func TestListSubscriptions(t *testing.T) {
now := time.Now()
testUser := &auth.User{
ID: "test-user",
GitHubUserID: nil,
}
testCases := []struct {
name string
cfg *MockListSavedSearchSubscriptionsConfig
expectedCallCount int
authMiddlewareOption testServerOption
request *http.Request
expectedResponse *http.Response
}{
{
name: "success",
cfg: &MockListSavedSearchSubscriptionsConfig{
expectedUserID: "test-user",
expectedPageSize: 100,
expectedPageToken: nil,
output: &backend.SubscriptionPage{
Data: &[]backend.SubscriptionResponse{
{
Id: "sub-id",
ChannelId: "channel-id",
SavedSearchId: "search-id",
Triggers: []backend.SubscriptionTriggerResponseItem{
{
Value: backendtypes.AttemptToStoreSubscriptionTrigger("trigger"),
RawValue: nil,
},
},
Frequency: "daily",
CreatedAt: now,
UpdatedAt: now,
},
},
Metadata: &backend.PageMetadata{
NextPageToken: nil,
},
},
err: nil,
},
expectedCallCount: 1,
request: httptest.NewRequest(
http.MethodGet,
"/v1/users/me/subscriptions",
nil,
),
expectedResponse: testJSONResponse(http.StatusOK,
`{
"data":[
{
"id":"sub-id",
"channel_id":"channel-id",
"saved_search_id":"search-id",
"triggers":[{"value":"trigger"}],
"frequency":"daily",
"created_at":"`+now.Format(time.RFC3339Nano)+`",
"updated_at":"`+now.Format(time.RFC3339Nano)+`"
}
],
"metadata":{}}`),
authMiddlewareOption: withAuthMiddleware(mockAuthMiddleware(testUser)),
},
{
name: "internal server error",
cfg: &MockListSavedSearchSubscriptionsConfig{
expectedUserID: "test-user",
expectedPageSize: 100,
expectedPageToken: nil,
output: nil,
err: errTest,
},
expectedCallCount: 1,
request: httptest.NewRequest(
http.MethodGet,
"/v1/users/me/subscriptions",
nil,
),
expectedResponse: testJSONResponse(http.StatusInternalServerError,
`{
"code":500,
"message":"could not list subscriptions"
}`),
authMiddlewareOption: withAuthMiddleware(mockAuthMiddleware(testUser)),
},
{
name: "success with page token and size",
cfg: &MockListSavedSearchSubscriptionsConfig{
expectedUserID: "test-user",
expectedPageSize: 50,
expectedPageToken: valuePtr("page-token"),
err: nil,
output: &backend.SubscriptionPage{
Data: &[]backend.SubscriptionResponse{
{
Id: "sub-id-2",
ChannelId: "channel-id-2",
SavedSearchId: "search-id-2",
Triggers: []backend.SubscriptionTriggerResponseItem{
{
Value: backendtypes.AttemptToStoreSubscriptionTrigger("trigger-2"),
RawValue: nil,
},
},
Frequency: "weekly",
CreatedAt: now,
UpdatedAt: now,
},
},
Metadata: &backend.PageMetadata{
NextPageToken: valuePtr("next-page-token"),
},
},
},
expectedCallCount: 1,
request: httptest.NewRequest(
http.MethodGet,
"/v1/users/me/subscriptions?page_size=50&page_token=page-token",
nil,
),
expectedResponse: testJSONResponse(http.StatusOK,
`{
"data":[
{
"id":"sub-id-2",
"channel_id":"channel-id-2",
"saved_search_id":"search-id-2",
"triggers":[{"value":"trigger-2"}],
"frequency":"weekly",
"created_at":"`+now.Format(time.RFC3339Nano)+`",
"updated_at":"`+now.Format(time.RFC3339Nano)+`"
}
],
"metadata":{
"next_page_token":"next-page-token"
}
}`),
authMiddlewareOption: withAuthMiddleware(mockAuthMiddleware(testUser)),
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
//nolint:exhaustruct
mockStorer := &MockWPTMetricsStorer{
listSavedSearchSubscriptionsCfg: tc.cfg,
t: t,
}
myServer := Server{
wptMetricsStorer: mockStorer,
metadataStorer: nil,
userGitHubClientFactory: nil,
operationResponseCaches: nil,
baseURL: getTestBaseURL(t),
}
assertTestServerRequest(t, &myServer, tc.request, tc.expectedResponse, tc.authMiddlewareOption)
assertMocksExpectations(t,
tc.expectedCallCount,
mockStorer.callCountListSavedSearchSubscriptions,
"ListSavedSearchSubscriptions",
nil)
})
}

}
10 changes: 10 additions & 0 deletions backend/pkg/httpserver/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1235,6 +1235,16 @@ func (m *mockServerInterface) ListMissingOneImplementationFeatures(ctx context.C
panic("unimplemented")
}

// ListSubscriptions implements backend.StrictServerInterface.
// nolint: ireturn // WONTFIX - generated method signature
func (m *mockServerInterface) ListSubscriptions(ctx context.Context,
_ backend.ListSubscriptionsRequestObject) (
backend.ListSubscriptionsResponseObject, error) {
assertUserInCtx(ctx, m.t, m.expectedUserInCtx)
m.callCount++
panic("unimplemented")
}

func (m *mockServerInterface) assertCallCount(expectedCallCount int) {
if m.callCount != expectedCallCount {
m.t.Errorf("expected mock server to be used %d times. only used %d times", expectedCallCount, m.callCount)
Expand Down
42 changes: 40 additions & 2 deletions openapi/backend/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1066,7 +1066,46 @@ paths:
/v1/users/me/subscriptions:
description: Operations for managing user subscriptions to saved searches.
# POST operation to create a new subscription (to be added in a future PR)
# GET operation to list user's subscriptions (to be added in a future PR)
# GET operation to list user's subscriptions
get:
summary: List subscriptions for a saved search
operationId: listSubscriptions
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/paginationTokenParam'
- $ref: '#/components/parameters/paginationSizeParam'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/SubscriptionPage'
'400':
description: Bad Input
content:
application/json:
schema:
$ref: '#/components/schemas/BasicErrorModel'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/BasicErrorModel'
'403':
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/BasicErrorModel'
'500':
description: Internal Service Error
content:
application/json:
schema:
$ref: '#/components/schemas/BasicErrorModel'
/v1/users/me/subscriptions/{subscription_id}:
description: Operations for managing a specific user subscription.
parameters:
Expand Down Expand Up @@ -1649,7 +1688,6 @@ components:
description: >
The original, raw value from the database. Only present if the 'value'
is 'unknown', allowing clients to identify and manage deprecated triggers.
nullable: true
required:
- value
SubscriptionBase:
Expand Down
Loading