fix: adjust classroom topic scan + manage upgrade scopes (#73) (thanks @salmonumbrella)
This commit is contained in:
parent
ce44ca2620
commit
f5f33ca7a6
@ -6,7 +6,9 @@
|
||||
|
||||
- Gmail: include `gmail.settings.sharing` scope for filter operations to avoid 403 insufficientPermissions. (#69) — thanks @ryanh-ai.
|
||||
- Gmail: resync on stale history 404s and skip missing message fetches without masking non-404 failures. (#70) — thanks @antons.
|
||||
- Auth: account manager upgrade respects managed services and skips Keep OAuth scopes. (#73) — thanks @salmonumbrella.
|
||||
- Classroom: normalize assignee updates + fix grade update masks. (#74) — thanks @salmonumbrella.
|
||||
- Classroom: scan pages when filtering coursework/materials by topic. (#73) — thanks @salmonumbrella.
|
||||
|
||||
### Build
|
||||
|
||||
|
||||
@ -185,13 +185,13 @@ Flag aliases:
|
||||
- `gog classroom teachers add <courseId> <userId>`
|
||||
- `gog classroom teachers remove <courseId> <userId>`
|
||||
- `gog classroom roster <courseId> [--students] [--teachers]`
|
||||
- `gog classroom coursework <courseId> [--state ...] [--topic TOPIC_ID] [--max N] [--page TOKEN]`
|
||||
- `gog classroom coursework <courseId> [--state ...] [--topic TOPIC_ID] [--scan-pages N] [--max N] [--page TOKEN]`
|
||||
- `gog classroom coursework get <courseId> <courseworkId>`
|
||||
- `gog classroom coursework create <courseId> --title TITLE [--type ASSIGNMENT|...]`
|
||||
- `gog classroom coursework update <courseId> <courseworkId> [--title ...]`
|
||||
- `gog classroom coursework delete <courseId> <courseworkId>`
|
||||
- `gog classroom coursework assignees <courseId> <courseworkId> [--mode ...] [--add-student ...]`
|
||||
- `gog classroom materials <courseId> [--state ...] [--topic TOPIC_ID] [--max N] [--page TOKEN]`
|
||||
- `gog classroom materials <courseId> [--state ...] [--topic TOPIC_ID] [--scan-pages N] [--max N] [--page TOKEN]`
|
||||
- `gog classroom materials get <courseId> <materialId>`
|
||||
- `gog classroom materials create <courseId> --title TITLE`
|
||||
- `gog classroom materials update <courseId> <materialId> [--title ...]`
|
||||
|
||||
@ -22,12 +22,13 @@ type ClassroomCourseworkCmd struct {
|
||||
}
|
||||
|
||||
type ClassroomCourseworkListCmd struct {
|
||||
CourseID string `arg:"" name:"courseId" help:"Course ID or alias"`
|
||||
States string `name:"state" help:"Coursework states filter (comma-separated: DRAFT,PUBLISHED,DELETED)"`
|
||||
Topic string `name:"topic" help:"Filter by topic ID"`
|
||||
OrderBy string `name:"order-by" help:"Order by (e.g., updateTime desc, dueDate desc)"`
|
||||
Max int64 `name:"max" aliases:"limit" help:"Max results" default:"100"`
|
||||
Page string `name:"page" help:"Page token"`
|
||||
CourseID string `arg:"" name:"courseId" help:"Course ID or alias"`
|
||||
States string `name:"state" help:"Coursework states filter (comma-separated: DRAFT,PUBLISHED,DELETED)"`
|
||||
Topic string `name:"topic" help:"Filter by topic ID"`
|
||||
OrderBy string `name:"order-by" help:"Order by (e.g., updateTime desc, dueDate desc)"`
|
||||
Max int64 `name:"max" aliases:"limit" help:"Max results" default:"100"`
|
||||
Page string `name:"page" help:"Page token"`
|
||||
ScanPages int `name:"scan-pages" help:"Pages to scan when filtering by topic" default:"3"`
|
||||
}
|
||||
|
||||
func (c *ClassroomCourseworkListCmd) Run(ctx context.Context, flags *RootFlags) error {
|
||||
@ -46,45 +47,71 @@ func (c *ClassroomCourseworkListCmd) Run(ctx context.Context, flags *RootFlags)
|
||||
return wrapClassroomError(err)
|
||||
}
|
||||
|
||||
call := svc.Courses.CourseWork.List(courseID).PageSize(c.Max).PageToken(c.Page).Context(ctx)
|
||||
if states := splitCSV(c.States); len(states) > 0 {
|
||||
upper := make([]string, 0, len(states))
|
||||
for _, state := range states {
|
||||
upper = append(upper, strings.ToUpper(state))
|
||||
makeCall := func(page string) *classroom.CoursesCourseWorkListCall {
|
||||
call := svc.Courses.CourseWork.List(courseID).PageSize(c.Max).PageToken(page).Context(ctx)
|
||||
if states := splitCSV(c.States); len(states) > 0 {
|
||||
upper := make([]string, 0, len(states))
|
||||
for _, state := range states {
|
||||
upper = append(upper, strings.ToUpper(state))
|
||||
}
|
||||
call.CourseWorkStates(upper...)
|
||||
}
|
||||
call.CourseWorkStates(upper...)
|
||||
}
|
||||
if v := strings.TrimSpace(c.OrderBy); v != "" {
|
||||
call.OrderBy(v)
|
||||
if v := strings.TrimSpace(c.OrderBy); v != "" {
|
||||
call.OrderBy(v)
|
||||
}
|
||||
return call
|
||||
}
|
||||
|
||||
resp, err := call.Do()
|
||||
if err != nil {
|
||||
return wrapClassroomError(err)
|
||||
}
|
||||
|
||||
// Client-side filter by topic (API doesn't support server-side topic filter)
|
||||
topicFilter := strings.TrimSpace(c.Topic)
|
||||
coursework := resp.CourseWork
|
||||
if topicFilter != "" {
|
||||
filtered := make([]*classroom.CourseWork, 0, len(coursework))
|
||||
for _, work := range coursework {
|
||||
pageToken := c.Page
|
||||
scanPages := c.ScanPages
|
||||
if scanPages <= 0 {
|
||||
scanPages = 1
|
||||
}
|
||||
|
||||
var (
|
||||
coursework []*classroom.CourseWork
|
||||
nextPageToken string
|
||||
)
|
||||
for page := 0; ; page++ {
|
||||
resp, err := makeCall(pageToken).Do()
|
||||
if err != nil {
|
||||
return wrapClassroomError(err)
|
||||
}
|
||||
nextPageToken = resp.NextPageToken
|
||||
|
||||
if topicFilter == "" {
|
||||
coursework = resp.CourseWork
|
||||
break
|
||||
}
|
||||
|
||||
filtered := make([]*classroom.CourseWork, 0, len(resp.CourseWork))
|
||||
for _, work := range resp.CourseWork {
|
||||
if work != nil && work.TopicId == topicFilter {
|
||||
filtered = append(filtered, work)
|
||||
}
|
||||
}
|
||||
coursework = filtered
|
||||
if len(filtered) > 0 {
|
||||
coursework = filtered
|
||||
break
|
||||
}
|
||||
if nextPageToken == "" || page+1 >= scanPages {
|
||||
coursework = filtered
|
||||
break
|
||||
}
|
||||
pageToken = nextPageToken
|
||||
}
|
||||
|
||||
if outfmt.IsJSON(ctx) {
|
||||
return outfmt.WriteJSON(os.Stdout, map[string]any{
|
||||
"coursework": coursework,
|
||||
"nextPageToken": resp.NextPageToken,
|
||||
"nextPageToken": nextPageToken,
|
||||
})
|
||||
}
|
||||
|
||||
if len(coursework) == 0 {
|
||||
u.Err().Println("No coursework")
|
||||
printNextPageHint(u, nextPageToken)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -104,7 +131,7 @@ func (c *ClassroomCourseworkListCmd) Run(ctx context.Context, flags *RootFlags)
|
||||
formatFloatValue(work.MaxPoints),
|
||||
)
|
||||
}
|
||||
printNextPageHint(u, resp.NextPageToken)
|
||||
printNextPageHint(u, nextPageToken)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -21,12 +21,13 @@ type ClassroomMaterialsCmd struct {
|
||||
}
|
||||
|
||||
type ClassroomMaterialsListCmd struct {
|
||||
CourseID string `arg:"" name:"courseId" help:"Course ID or alias"`
|
||||
States string `name:"state" help:"Material states filter (comma-separated: PUBLISHED,DRAFT,DELETED)"`
|
||||
Topic string `name:"topic" help:"Filter by topic ID"`
|
||||
OrderBy string `name:"order-by" help:"Order by (e.g., updateTime desc)"`
|
||||
Max int64 `name:"max" aliases:"limit" help:"Max results" default:"100"`
|
||||
Page string `name:"page" help:"Page token"`
|
||||
CourseID string `arg:"" name:"courseId" help:"Course ID or alias"`
|
||||
States string `name:"state" help:"Material states filter (comma-separated: PUBLISHED,DRAFT,DELETED)"`
|
||||
Topic string `name:"topic" help:"Filter by topic ID"`
|
||||
OrderBy string `name:"order-by" help:"Order by (e.g., updateTime desc)"`
|
||||
Max int64 `name:"max" aliases:"limit" help:"Max results" default:"100"`
|
||||
Page string `name:"page" help:"Page token"`
|
||||
ScanPages int `name:"scan-pages" help:"Pages to scan when filtering by topic" default:"3"`
|
||||
}
|
||||
|
||||
func (c *ClassroomMaterialsListCmd) Run(ctx context.Context, flags *RootFlags) error {
|
||||
@ -45,45 +46,71 @@ func (c *ClassroomMaterialsListCmd) Run(ctx context.Context, flags *RootFlags) e
|
||||
return wrapClassroomError(err)
|
||||
}
|
||||
|
||||
call := svc.Courses.CourseWorkMaterials.List(courseID).PageSize(c.Max).PageToken(c.Page).Context(ctx)
|
||||
if states := splitCSV(c.States); len(states) > 0 {
|
||||
upper := make([]string, 0, len(states))
|
||||
for _, state := range states {
|
||||
upper = append(upper, strings.ToUpper(state))
|
||||
makeCall := func(page string) *classroom.CoursesCourseWorkMaterialsListCall {
|
||||
call := svc.Courses.CourseWorkMaterials.List(courseID).PageSize(c.Max).PageToken(page).Context(ctx)
|
||||
if states := splitCSV(c.States); len(states) > 0 {
|
||||
upper := make([]string, 0, len(states))
|
||||
for _, state := range states {
|
||||
upper = append(upper, strings.ToUpper(state))
|
||||
}
|
||||
call.CourseWorkMaterialStates(upper...)
|
||||
}
|
||||
call.CourseWorkMaterialStates(upper...)
|
||||
}
|
||||
if v := strings.TrimSpace(c.OrderBy); v != "" {
|
||||
call.OrderBy(v)
|
||||
if v := strings.TrimSpace(c.OrderBy); v != "" {
|
||||
call.OrderBy(v)
|
||||
}
|
||||
return call
|
||||
}
|
||||
|
||||
resp, err := call.Do()
|
||||
if err != nil {
|
||||
return wrapClassroomError(err)
|
||||
}
|
||||
|
||||
// Client-side filter by topic (API doesn't support server-side topic filter)
|
||||
topicFilter := strings.TrimSpace(c.Topic)
|
||||
materials := resp.CourseWorkMaterial
|
||||
if topicFilter != "" {
|
||||
filtered := make([]*classroom.CourseWorkMaterial, 0, len(materials))
|
||||
for _, material := range materials {
|
||||
pageToken := c.Page
|
||||
scanPages := c.ScanPages
|
||||
if scanPages <= 0 {
|
||||
scanPages = 1
|
||||
}
|
||||
|
||||
var (
|
||||
materials []*classroom.CourseWorkMaterial
|
||||
nextPageToken string
|
||||
)
|
||||
for page := 0; ; page++ {
|
||||
resp, err := makeCall(pageToken).Do()
|
||||
if err != nil {
|
||||
return wrapClassroomError(err)
|
||||
}
|
||||
nextPageToken = resp.NextPageToken
|
||||
|
||||
if topicFilter == "" {
|
||||
materials = resp.CourseWorkMaterial
|
||||
break
|
||||
}
|
||||
|
||||
filtered := make([]*classroom.CourseWorkMaterial, 0, len(resp.CourseWorkMaterial))
|
||||
for _, material := range resp.CourseWorkMaterial {
|
||||
if material != nil && material.TopicId == topicFilter {
|
||||
filtered = append(filtered, material)
|
||||
}
|
||||
}
|
||||
materials = filtered
|
||||
if len(filtered) > 0 {
|
||||
materials = filtered
|
||||
break
|
||||
}
|
||||
if nextPageToken == "" || page+1 >= scanPages {
|
||||
materials = filtered
|
||||
break
|
||||
}
|
||||
pageToken = nextPageToken
|
||||
}
|
||||
|
||||
if outfmt.IsJSON(ctx) {
|
||||
return outfmt.WriteJSON(os.Stdout, map[string]any{
|
||||
"materials": materials,
|
||||
"nextPageToken": resp.NextPageToken,
|
||||
"nextPageToken": nextPageToken,
|
||||
})
|
||||
}
|
||||
|
||||
if len(materials) == 0 {
|
||||
u.Err().Println("No materials")
|
||||
printNextPageHint(u, nextPageToken)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -101,7 +128,7 @@ func (c *ClassroomMaterialsListCmd) Run(ctx context.Context, flags *RootFlags) e
|
||||
sanitizeTab(material.UpdateTime),
|
||||
)
|
||||
}
|
||||
printNextPageHint(u, resp.NextPageToken)
|
||||
printNextPageHint(u, nextPageToken)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
143
internal/cmd/classroom_topic_scan_test.go
Normal file
143
internal/cmd/classroom_topic_scan_test.go
Normal file
@ -0,0 +1,143 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"google.golang.org/api/classroom/v1"
|
||||
"google.golang.org/api/option"
|
||||
)
|
||||
|
||||
func TestClassroomCourseworkList_TopicScanPages(t *testing.T) {
|
||||
origNew := newClassroomService
|
||||
t.Cleanup(func() { newClassroomService = origNew })
|
||||
|
||||
var calls int
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.Contains(r.URL.Path, "/courseWork") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
calls++
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
switch calls {
|
||||
case 1:
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"courseWork": []map[string]any{{"id": "w1", "topicId": "other"}},
|
||||
"nextPageToken": "p2",
|
||||
})
|
||||
case 2:
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"courseWork": []map[string]any{{"id": "w2", "topicId": "target"}},
|
||||
"nextPageToken": "",
|
||||
})
|
||||
default:
|
||||
t.Fatalf("unexpected coursework calls: %d", calls)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
svc, err := classroom.NewService(context.Background(),
|
||||
option.WithoutAuthentication(),
|
||||
option.WithHTTPClient(srv.Client()),
|
||||
option.WithEndpoint(srv.URL+"/"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewService: %v", err)
|
||||
}
|
||||
newClassroomService = func(context.Context, string) (*classroom.Service, error) { return svc, nil }
|
||||
|
||||
var payload struct {
|
||||
Coursework []struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"coursework"`
|
||||
NextPageToken string `json:"nextPageToken"`
|
||||
}
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "classroom", "coursework", "c1", "--topic", "target", "--scan-pages", "2"}); err != nil {
|
||||
t.Fatalf("execute: %v", err)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if err := json.Unmarshal([]byte(out), &payload); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if len(payload.Coursework) != 1 || payload.Coursework[0].ID != "w2" {
|
||||
t.Fatalf("expected coursework w2, got %#v", payload.Coursework)
|
||||
}
|
||||
if calls != 2 {
|
||||
t.Fatalf("expected 2 calls, got %d", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassroomMaterialsList_TopicScanPages(t *testing.T) {
|
||||
origNew := newClassroomService
|
||||
t.Cleanup(func() { newClassroomService = origNew })
|
||||
|
||||
var calls int
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.Contains(r.URL.Path, "/courseWorkMaterials") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
calls++
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
switch calls {
|
||||
case 1:
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"courseWorkMaterial": []map[string]any{{"id": "m1", "topicId": "other"}},
|
||||
"nextPageToken": "p2",
|
||||
})
|
||||
case 2:
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"courseWorkMaterial": []map[string]any{{"id": "m2", "topicId": "target"}},
|
||||
"nextPageToken": "",
|
||||
})
|
||||
default:
|
||||
t.Fatalf("unexpected materials calls: %d", calls)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
svc, err := classroom.NewService(context.Background(),
|
||||
option.WithoutAuthentication(),
|
||||
option.WithHTTPClient(srv.Client()),
|
||||
option.WithEndpoint(srv.URL+"/"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewService: %v", err)
|
||||
}
|
||||
newClassroomService = func(context.Context, string) (*classroom.Service, error) { return svc, nil }
|
||||
|
||||
var payload struct {
|
||||
Materials []struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"materials"`
|
||||
NextPageToken string `json:"nextPageToken"`
|
||||
}
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "classroom", "materials", "c1", "--topic", "target", "--scan-pages", "2"}); err != nil {
|
||||
t.Fatalf("execute: %v", err)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if err := json.Unmarshal([]byte(out), &payload); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if len(payload.Materials) != 1 || payload.Materials[0].ID != "m2" {
|
||||
t.Fatalf("expected material m2, got %#v", payload.Materials)
|
||||
}
|
||||
if calls != 2 {
|
||||
t.Fatalf("expected 2 calls, got %d", calls)
|
||||
}
|
||||
}
|
||||
@ -123,8 +123,8 @@ func TestExecute_ClassroomMoreCommands_JSON(t *testing.T) {
|
||||
writeJSON(map[string]any{"ok": true})
|
||||
return
|
||||
case strings.Contains(path, "/studentSubmissions/") && r.Method == http.MethodPatch:
|
||||
if got := r.URL.Query().Get("updateMask"); got != "draft_grade,assigned_grade" {
|
||||
t.Fatalf("expected updateMask draft_grade,assigned_grade, got %q", got)
|
||||
if got := r.URL.Query().Get("updateMask"); got != "draftGrade,assignedGrade" {
|
||||
t.Fatalf("expected updateMask draftGrade,assignedGrade, got %q", got)
|
||||
}
|
||||
writeJSON(map[string]any{"id": "s1", "draftGrade": 5, "assignedGrade": 10})
|
||||
return
|
||||
|
||||
@ -74,6 +74,26 @@ func shouldEnsureKeychainAccess() (bool, error) {
|
||||
return backendInfo.Value != "file", nil
|
||||
}
|
||||
|
||||
func manageServices(services []Service) []Service {
|
||||
if len(services) == 0 {
|
||||
services = UserServices()
|
||||
}
|
||||
|
||||
filtered := make([]Service, 0, len(services))
|
||||
for _, svc := range services {
|
||||
if svc == ServiceKeep {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, svc)
|
||||
}
|
||||
|
||||
if len(filtered) == 0 {
|
||||
return UserServices()
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
// StartManageServer starts the accounts management server and opens browser
|
||||
func StartManageServer(ctx context.Context, opts ManageServerOptions) error {
|
||||
if opts.Timeout <= 0 {
|
||||
@ -220,10 +240,7 @@ func (ms *ManageServer) handleAuthStart(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
ms.oauthState = state
|
||||
|
||||
services := ms.opts.Services
|
||||
if len(services) == 0 {
|
||||
services = AllServices()
|
||||
}
|
||||
services := manageServices(ms.opts.Services)
|
||||
|
||||
scopes, err := ScopesForManage(services)
|
||||
if err != nil {
|
||||
@ -267,8 +284,8 @@ func (ms *ManageServer) handleAuthUpgrade(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
ms.oauthState = state
|
||||
|
||||
// Always use all services for upgrade
|
||||
services := AllServices()
|
||||
// Use requested manage services (exclude Keep)
|
||||
services := manageServices(ms.opts.Services)
|
||||
|
||||
scopes, err := ScopesForManage(services)
|
||||
if err != nil {
|
||||
@ -331,10 +348,7 @@ func (ms *ManageServer) handleOAuthCallback(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
services := ms.opts.Services
|
||||
if len(services) == 0 {
|
||||
services = AllServices()
|
||||
}
|
||||
services := manageServices(ms.opts.Services)
|
||||
|
||||
scopes, err := ScopesForManage(services)
|
||||
if err != nil {
|
||||
@ -618,9 +632,11 @@ func renderSuccessPageWithDetails(w http.ResponseWriter, email string, services
|
||||
return
|
||||
}
|
||||
|
||||
// Get all available services for showing connected vs missing
|
||||
allServices := make([]string, 0, len(serviceOrder))
|
||||
for _, svc := range serviceOrder {
|
||||
// Show available user services for connected vs missing
|
||||
userServices := UserServices()
|
||||
allServices := make([]string, 0, len(userServices))
|
||||
|
||||
for _, svc := range userServices {
|
||||
allServices = append(allServices, string(svc))
|
||||
}
|
||||
|
||||
|
||||
@ -428,12 +428,10 @@ func TestManageServer_HandleAuthStart(t *testing.T) {
|
||||
t.Fatalf("status: %d", rr.Code)
|
||||
}
|
||||
loc := rr.Header().Get("Location")
|
||||
var parsed *url.URL
|
||||
|
||||
if p, err := url.Parse(loc); err != nil {
|
||||
t.Fatalf("parse location: %v", err)
|
||||
} else {
|
||||
parsed = p
|
||||
parsed, parseErr := url.Parse(loc)
|
||||
if parseErr != nil {
|
||||
t.Fatalf("parse location: %v", parseErr)
|
||||
}
|
||||
|
||||
if parsed.Host != "example.com" {
|
||||
@ -884,7 +882,10 @@ func TestManageServer_HandleAuthUpgrade(t *testing.T) {
|
||||
|
||||
t.Cleanup(func() { _ = ln.Close() })
|
||||
|
||||
ms := &ManageServer{listener: ln}
|
||||
ms := &ManageServer{
|
||||
listener: ln,
|
||||
opts: ManageServerOptions{Services: []Service{ServiceGmail}},
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/auth/upgrade?email=test@example.com", nil)
|
||||
ms.handleAuthUpgrade(rr, req)
|
||||
@ -894,12 +895,10 @@ func TestManageServer_HandleAuthUpgrade(t *testing.T) {
|
||||
}
|
||||
|
||||
loc := rr.Header().Get("Location")
|
||||
var parsed *url.URL
|
||||
|
||||
if p, err := url.Parse(loc); err != nil {
|
||||
t.Fatalf("parse location: %v", err)
|
||||
} else {
|
||||
parsed = p
|
||||
parsed, parseErr := url.Parse(loc)
|
||||
if parseErr != nil {
|
||||
t.Fatalf("parse location: %v", parseErr)
|
||||
}
|
||||
|
||||
if parsed.Host != "example.com" {
|
||||
@ -914,6 +913,28 @@ func TestManageServer_HandleAuthUpgrade(t *testing.T) {
|
||||
t.Fatalf("expected oauthState set")
|
||||
}
|
||||
|
||||
scope := parsed.Query().Get("scope")
|
||||
|
||||
expectedScopes, err := ScopesForManage([]Service{ServiceGmail})
|
||||
if err != nil {
|
||||
t.Fatalf("ScopesForManage: %v", err)
|
||||
}
|
||||
|
||||
scopeSet := make(map[string]bool, len(expectedScopes))
|
||||
for _, s := range strings.Fields(scope) {
|
||||
scopeSet[s] = true
|
||||
}
|
||||
|
||||
for _, s := range expectedScopes {
|
||||
if !scopeSet[s] {
|
||||
t.Fatalf("expected scope %q in %q", s, scope)
|
||||
}
|
||||
}
|
||||
|
||||
if scopeSet["https://www.googleapis.com/auth/keep.readonly"] {
|
||||
t.Fatalf("unexpected keep scope in %q", scope)
|
||||
}
|
||||
|
||||
// Check for login_hint (pre-selects the email)
|
||||
if loginHint := parsed.Query().Get("login_hint"); loginHint != "test@example.com" {
|
||||
t.Fatalf("expected login_hint=test@example.com, got %q", loginHint)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user