internal/scan: move request to worker

scan.Request was specific to vulncheck, so move it to vulncheck_scan.go
in internal/worker.

Change-Id: I7e7f2bb4d9fb933368cd332b4211f89aeea78122
Reviewed-on: https://go-review.googlesource.com/c/pkgsite-metrics/+/467296
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Julie Qiu <julieqiu@google.com>
diff --git a/internal/queue/queue_test.go b/internal/queue/queue_test.go
index 5b91c65..a7cbf48 100644
--- a/internal/queue/queue_test.go
+++ b/internal/queue/queue_test.go
@@ -9,7 +9,6 @@
 
 	"github.com/google/go-cmp/cmp"
 	"golang.org/x/pkgsite-metrics/internal/config"
-	"golang.org/x/pkgsite-metrics/internal/scan"
 	taskspb "google.golang.org/genproto/googleapis/cloud/tasks/v2"
 	"google.golang.org/protobuf/testing/protocmp"
 	"google.golang.org/protobuf/types/known/durationpb"
@@ -79,16 +78,10 @@
 		Namespace:      "test",
 		TaskNameSuffix: "suf",
 	}
-	sreq := &scan.Request{
-		ModuleURLPath: scan.ModuleURLPath{
-			Module:  "mod",
-			Version: "v1.2.3",
-		},
-		RequestParams: scan.RequestParams{
-			ImportedBy: 0,
-			Mode:       "test",
-			Insecure:   true,
-		},
+	sreq := &testTask{
+		name:   "name",
+		path:   "mod@v1.2.3",
+		params: "importedby=0&mode=test&insecure=true",
 	}
 	got, err := gcp.newTaskRequest(sreq, opts)
 	if err != nil {
diff --git a/internal/scan/parse.go b/internal/scan/parse.go
index 8b27e73..d0e29f8 100644
--- a/internal/scan/parse.go
+++ b/internal/scan/parse.go
@@ -7,7 +7,6 @@
 
 import (
 	"bufio"
-	"errors"
 	"fmt"
 	"net/http"
 	"os"
@@ -19,65 +18,6 @@
 	"golang.org/x/pkgsite-metrics/internal/version"
 )
 
-// Request contains information passed
-// to a scan endpoint.
-type Request struct {
-	ModuleURLPath
-	RequestParams
-}
-
-// RequestParams has query parameters for a scan request.
-type RequestParams struct {
-	ImportedBy int
-	Mode       string
-	Insecure   bool
-}
-
-// These methods implement queue.Task.
-func (r *Request) Name() string { return r.Module + "@" + r.Version }
-
-func (r *Request) Path() string {
-	p := r.Module + "@" + r.Version
-	if r.Suffix != "" {
-		p += "/" + r.Suffix
-	}
-	return p
-}
-
-func (r *Request) Params() string {
-	return FormatParams(r.RequestParams)
-}
-
-// ParseRequest parses an http request r for an endpoint
-// scanPrefix and produces a corresponding ScanRequest.
-//
-// The module and version should have one of the following three forms:
-//   - <module>/@v/<version>
-//   - <module>@<version>
-//   - <module>/@latest
-//
-// (These are the same forms that the module proxy accepts.)
-func ParseRequest(r *http.Request, scanPrefix string) (_ *Request, err error) {
-	defer derrors.Wrap(&err, "ParseRequest(%s)", scanPrefix)
-
-	mp, err := ParseModuleURLPath(strings.TrimPrefix(r.URL.Path, scanPrefix))
-	if err != nil {
-		return nil, err
-	}
-
-	rp := RequestParams{ImportedBy: -1} // use -1 to detect missing param (explicit 0 is OK)
-	if err := ParseParams(r, &rp); err != nil {
-		return nil, err
-	}
-	if rp.ImportedBy < 0 {
-		return nil, errors.New(`missing or negative "importedby" query param`)
-	}
-	return &Request{
-		ModuleURLPath: mp,
-		RequestParams: rp,
-	}, nil
-}
-
 func ParseRequiredIntParam(r *http.Request, name string) (int, error) {
 	value := r.FormValue(name)
 	if value == "" {
diff --git a/internal/scan/parse_test.go b/internal/scan/parse_test.go
index f25967a..253f61c 100644
--- a/internal/scan/parse_test.go
+++ b/internal/scan/parse_test.go
@@ -6,7 +6,6 @@
 
 import (
 	"net/http"
-	"net/url"
 	"reflect"
 	"strings"
 	"testing"
@@ -15,113 +14,78 @@
 	"golang.org/x/pkgsite-metrics/internal/version"
 )
 
-func TestParseScanRequest(t *testing.T) {
+func TestParseModuleURLPath(t *testing.T) {
 	for _, test := range []struct {
-		name string
-		url  string
-		want Request
+		path string
+		want ModuleURLPath
 	}{
 		{
-			name: "ValidScanURL",
-			url:  "https://worker.com/scan/module/@v/v1.0.0?importedby=50",
-			want: Request{
-				ModuleURLPath{Module: "module", Version: "v1.0.0"},
-				RequestParams{ImportedBy: 50, Mode: ""},
+			"/module/@v/v1.0.0",
+			ModuleURLPath{Module: "module", Version: "v1.0.0"},
+		},
+		{
+			"/module/@v/v1.0.0-abcdefgh/suffix",
+			ModuleURLPath{
+				Module:  "module",
+				Version: "v1.0.0-abcdefgh",
+				Suffix:  "suffix",
 			},
 		},
 		{
-			name: "ValidImportsOnlyScanURL",
-			url:  "https://worker.com/scan/module/@v/v1.0.0-abcdefgh?importedby=100&mode=mode1",
-			want: Request{
-				ModuleURLPath{Module: "module", Version: "v1.0.0-abcdefgh"},
-				RequestParams{ImportedBy: 100, Mode: "mode1"},
-			},
-		},
-		{
-			name: "Module@Version",
-			url:  "https://worker.com/scan/module@v1.2.3?importedby=1",
-			want: Request{
-				ModuleURLPath{Module: "module", Version: "v1.2.3"},
-				RequestParams{ImportedBy: 1, Mode: ""},
-			},
-		},
-		{
-			name: "Module@Version suffix",
-			url:  "https://worker.com/scan/module@v1.2.3/path/to/dir?importedby=1",
-			want: Request{
-				ModuleURLPath{Module: "module", Version: "v1.2.3", Suffix: "path/to/dir"},
-				RequestParams{ImportedBy: 1, Mode: ""},
+			"/module@v1.2.3/a/b/c",
+			ModuleURLPath{
+				Module:  "module",
+				Version: "v1.2.3",
+				Suffix:  "a/b/c",
 			},
 		},
 	} {
-		t.Run(test.name, func(t *testing.T) {
-			u, err := url.Parse(test.url)
-			if err != nil {
-				t.Errorf("url.Parse(%q): %v", test.url, err)
-			}
-			r := &http.Request{URL: u}
-			got, err := ParseRequest(r, "/scan")
-			if err != nil {
-				t.Fatal(err)
-			}
-			if g, w := *got, test.want; g != w {
-				t.Errorf("\ngot  %+v\nwant %+v", g, w)
-			}
-		})
+		got, err := ParseModuleURLPath(test.path)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if g, w := got, test.want; !cmp.Equal(g, w) {
+			t.Errorf("%s:\ngot  %+v\nwant %+v", test.path, g, w)
+		}
 	}
 }
 
-func TestParseScanRequestError(t *testing.T) {
+func TestModuleURLPAthError(t *testing.T) {
 	for _, test := range []struct {
 		name string
-		url  string
+		path string
 		want string
 	}{
 		{
 			name: "InvalidScanURL",
-			url:  "/",
+			path: "/",
 			want: `invalid path "/": missing '@'`,
 		},
 		{
 			name: "InvalidScanURLNoModule",
-			url:  "/@v/version",
+			path: "/@v/version",
 			want: `invalid path "/@v/version": missing module`,
 		},
 		{
 			name: "InvalidScanURLNoVersion",
-			url:  "/module/@v/",
+			path: "/module/@v/",
 			want: `invalid path "/module/@v/": missing version`,
 		},
 		{
 			name: "NoVersion",
-			url:  "/module@",
+			path: "/module@",
 			want: `invalid path "/module@": missing version`,
 		},
 		{
 			name: "NoVersionSuffix",
-			url:  "/module@/suffix",
+			path: "/module@/suffix",
 			want: `invalid path "/module@/suffix": missing version`,
 		},
-		{
-			name: "MissingImportedBy",
-			url:  "/module/@v/v1.0.0",
-			want: `missing or negative "importedby" query param`,
-		},
-		{
-			name: "BadImportedBy",
-			url:  "/module@v1?importedby=1a",
-			want: `param importedby: strconv.Atoi: parsing "1a": invalid syntax`,
-		},
 	} {
 		t.Run(test.name, func(t *testing.T) {
-			u, err := url.Parse(test.url)
-			if err != nil {
-				t.Errorf("url.Parse(%q): %v", test.url, err)
-			}
-			r := &http.Request{URL: u}
-			if _, err := ParseRequest(r, "/scan"); err != nil {
-				if got := err.Error(); !strings.Contains(got, test.want) {
-					t.Fatalf("\ngot  %s\nwant %s", got, test.want)
+			if _, err := ParseModuleURLPath(test.path); err != nil {
+				if got := err.Error(); !strings.HasSuffix(got, test.want) {
+					t.Fatalf("\ngot  %s\nwant suffix %s", got, test.want)
 				}
 			} else {
 				t.Fatalf("error = nil; want = (%v)", test.want)
diff --git a/internal/worker/enqueue.go b/internal/worker/enqueue.go
index 804fab7..814ee9e 100644
--- a/internal/worker/enqueue.go
+++ b/internal/worker/enqueue.go
@@ -6,7 +6,6 @@
 
 import (
 	"context"
-	"errors"
 	"sync"
 
 	"golang.org/x/pkgsite-metrics/internal/config"
@@ -37,8 +36,8 @@
 	return pkgsitedb.ModuleSpecs(ctx, db, minImportedByCount)
 }
 
-func enqueueModules(ctx context.Context, sreqs []*scan.Request, q queue.Queue, opts *queue.Options) (err error) {
-	defer derrors.Wrap(&err, "enqueueModules")
+func enqueueTasks(ctx context.Context, tasks []queue.Task, q queue.Queue, opts *queue.Options) (err error) {
+	defer derrors.Wrap(&err, "enqueueTasks")
 
 	// Enqueue concurrently, because sequentially takes a while.
 	const concurrentEnqueues = 10
@@ -48,14 +47,8 @@
 	)
 	sem := make(chan struct{}, concurrentEnqueues)
 
-	for _, sreq := range sreqs {
+	for _, sreq := range tasks {
 		log.Infof(ctx, "enqueuing: %s?%s", sreq.Path(), sreq.Params())
-		if sreq.Module == "std" {
-			continue // ignore the standard library
-		}
-		if sreq.Mode == "" {
-			return errors.New("ScanRequest.Mode cannot be empty")
-		}
 		sreq := sreq
 		sem <- struct{}{}
 		go func() {
@@ -79,12 +72,18 @@
 	return nil
 }
 
-func moduleSpecsToScanRequests(modspecs []scan.ModuleSpec, mode string) []*scan.Request {
-	var sreqs []*scan.Request
+func moduleSpecsToScanRequests(modspecs []scan.ModuleSpec, mode string) []*vulncheckRequest {
+	var sreqs []*vulncheckRequest
 	for _, ms := range modspecs {
-		sreqs = append(sreqs, &scan.Request{
-			ModuleURLPath: scan.ModuleURLPath{Module: ms.Path, Version: ms.Version},
-			RequestParams: scan.RequestParams{ImportedBy: ms.ImportedBy, Mode: mode},
+		sreqs = append(sreqs, &vulncheckRequest{
+			scan.ModuleURLPath{
+				Module:  ms.Path,
+				Version: ms.Version,
+			},
+			vulncheckRequestParams{
+				ImportedBy: ms.ImportedBy,
+				Mode:       mode,
+			},
 		})
 	}
 	return sreqs
diff --git a/internal/worker/vulncheck_enqueue.go b/internal/worker/vulncheck_enqueue.go
index 778b252..b3ddb54 100644
--- a/internal/worker/vulncheck_enqueue.go
+++ b/internal/worker/vulncheck_enqueue.go
@@ -18,39 +18,48 @@
 	"google.golang.org/api/iterator"
 )
 
+// query params for vulncheck/enqueue
+type vulncheckEnqueueParams struct {
+	Suffix string // appended to task queue IDs to generate unique tasks
+	Mode   string // type of analysis to run
+	Min    int    // minimum import-by count for a module to be included
+	File   string // path to file containing modules; if missing, use DB
+}
+
 // handleEnqueue enqueues multiple modules for a single vulncheck mode.
-// Query params:
-//   - suffix: appended to task queue IDs to generate unique tasks
-//   - mode: type of analysis to run; see [modes]
-//   - file: path to file containing modules; if missing, use DB
-//   - min: minimum import-by count for a module to be included
 func (h *VulncheckServer) handleEnqueue(w http.ResponseWriter, r *http.Request) error {
+	params := &vulncheckEnqueueParams{Min: defaultMinImportedByCount}
+	if err := scan.ParseParams(r, &params); err != nil {
+		return err
+	}
 	ctx := r.Context()
-	suffix := r.FormValue("suffix")
-	mode, err := vulncheckMode(scan.ParseMode(r))
+	mode, err := vulncheckMode(params.Mode)
 	if err != nil {
 		return err
 	}
 
-	var sreqs []*scan.Request
+	var reqs []*vulncheckRequest
 	if mode == ModeBinary {
 		var err error
-		sreqs, err = readBinaries(ctx, h.cfg.BinaryBucket)
+		reqs, err = readBinaries(ctx, h.cfg.BinaryBucket)
 		if err != nil {
 			return err
 		}
 	} else {
-		minImpCount, err := scan.ParseOptionalIntParam(r, "min", defaultMinImportedByCount)
+		modspecs, err := readModules(ctx, h.cfg, params.File, params.Min)
 		if err != nil {
 			return err
 		}
-		modspecs, err := readModules(ctx, h.cfg, r.FormValue("file"), minImpCount)
-		if err != nil {
-			return err
-		}
-		sreqs = moduleSpecsToScanRequests(modspecs, mode)
+		reqs = moduleSpecsToScanRequests(modspecs, mode)
 	}
-	return enqueueModules(ctx, sreqs, h.queue, &queue.Options{Namespace: "vulncheck", TaskNameSuffix: suffix})
+	var sreqs []queue.Task
+	for _, req := range reqs {
+		if req.Module != "std" { // ignore the standard library
+			sreqs = append(sreqs, req)
+		}
+	}
+	return enqueueTasks(ctx, sreqs, h.queue,
+		&queue.Options{Namespace: "vulncheck", TaskNameSuffix: params.Suffix})
 }
 
 func vulncheckMode(mode string) (string, error) {
@@ -71,28 +80,34 @@
 //   - file: path to file containing modules; if missing, use DB
 //   - min: minimum import-by count for a module to be included
 func (h *VulncheckServer) handleEnqueueAll(w http.ResponseWriter, r *http.Request) error {
+	params := &vulncheckEnqueueParams{Min: defaultMinImportedByCount}
+	if err := scan.ParseParams(r, &params); err != nil {
+		return err
+	}
+
 	ctx := r.Context()
-	suffix := r.FormValue("suffix")
-	minImpCount, err := scan.ParseOptionalIntParam(r, "min", defaultMinImportedByCount)
+	modspecs, err := readModules(ctx, h.cfg, params.File, params.Min)
 	if err != nil {
 		return err
 	}
-	modspecs, err := readModules(ctx, h.cfg, r.FormValue("file"), minImpCount)
-	if err != nil {
-		return err
-	}
-	opts := &queue.Options{Namespace: "vulncheck", TaskNameSuffix: suffix}
+	opts := &queue.Options{Namespace: "vulncheck", TaskNameSuffix: params.Suffix}
 	for mode := range modes {
-		var sreqs []*scan.Request
+		var reqs []*vulncheckRequest
 		if mode == ModeBinary {
-			sreqs, err = readBinaries(ctx, h.cfg.BinaryBucket)
+			reqs, err = readBinaries(ctx, h.cfg.BinaryBucket)
 			if err != nil {
 				return err
 			}
 		} else {
-			sreqs = moduleSpecsToScanRequests(modspecs, mode)
+			reqs = moduleSpecsToScanRequests(modspecs, mode)
 		}
-		if err := enqueueModules(ctx, sreqs, h.queue, opts); err != nil {
+		var tasks []queue.Task
+		for _, req := range reqs {
+			if req.Module != "std" { // ignore the standard library
+				tasks = append(tasks, req)
+			}
+		}
+		if err := enqueueTasks(ctx, tasks, h.queue, opts); err != nil {
 			return err
 		}
 	}
@@ -102,7 +117,7 @@
 // binaryDir is the directory in the GCS bucket that contains binaries that should be scanned.
 const binaryDir = "binaries"
 
-func readBinaries(ctx context.Context, bucketName string) (sreqs []*scan.Request, err error) {
+func readBinaries(ctx context.Context, bucketName string) (reqs []*vulncheckRequest, err error) {
 	defer derrors.Wrap(&err, "readBinaries(%q)", bucketName)
 	if bucketName == "" {
 		log.Infof(ctx, "binary bucket not configured; not enqueuing binaries")
@@ -125,10 +140,10 @@
 		if err != nil {
 			return nil, err
 		}
-		sreqs = append(sreqs, &scan.Request{
-			ModuleURLPath: mp,
-			RequestParams: scan.RequestParams{Mode: ModeBinary},
+		reqs = append(reqs, &vulncheckRequest{
+			ModuleURLPath:          mp,
+			vulncheckRequestParams: vulncheckRequestParams{Mode: ModeBinary},
 		})
 	}
-	return sreqs, nil
+	return reqs, nil
 }
diff --git a/internal/worker/vulncheck_enqueue_test.go b/internal/worker/vulncheck_enqueue_test.go
index f178601..931e9df 100644
--- a/internal/worker/vulncheck_enqueue_test.go
+++ b/internal/worker/vulncheck_enqueue_test.go
@@ -22,13 +22,13 @@
 	if err != nil {
 		t.Fatal(err)
 	}
-	want := &scan.Request{
-		ModuleURLPath: scan.ModuleURLPath{
+	want := &vulncheckRequest{
+		scan.ModuleURLPath{
 			Module:  "golang.org/x/pkgsite",
 			Version: "v0.0.0-20221004150836-873fb37c2479",
 			Suffix:  "cmd/worker",
 		},
-		RequestParams: scan.RequestParams{Mode: ModeBinary},
+		vulncheckRequestParams{Mode: ModeBinary},
 	}
 	found := false
 	for _, sr := range sreqs {
diff --git a/internal/worker/vulncheck_scan.go b/internal/worker/vulncheck_scan.go
index 200bb5c..e4aca86 100644
--- a/internal/worker/vulncheck_scan.go
+++ b/internal/worker/vulncheck_scan.go
@@ -84,7 +84,7 @@
 	}()
 
 	ctx := r.Context()
-	sreq, err := scan.ParseRequest(r, "/vulncheck/scan")
+	sreq, err := parseVulncheckRequest(r, "/vulncheck/scan")
 	if err != nil {
 		return fmt.Errorf("%w: %v", derrors.InvalidArgument, err)
 	}
@@ -184,7 +184,7 @@
 	return s.err
 }
 
-func (s *scanner) ScanModule(ctx context.Context, sreq *scan.Request) {
+func (s *scanner) ScanModule(ctx context.Context, sreq *vulncheckRequest) {
 	if sreq.Module == "std" {
 		return // ignore the standard library
 	}
@@ -716,7 +716,7 @@
 // This runs a scan but returns the resulting JSON instead of writing it to BigQuery.
 func (s *Server) handleTestVulncheckSandbox(w http.ResponseWriter, r *http.Request) error {
 	ctx := r.Context()
-	sreq, err := scan.ParseRequest(r, "/test-vulncheck-sandbox")
+	sreq, err := parseVulncheckRequest(r, "/test-vulncheck-sandbox")
 	if err != nil {
 		return fmt.Errorf("%w: %v", derrors.InvalidArgument, err)
 	}
@@ -729,3 +729,61 @@
 	_, err = w.Write(out)
 	return err
 }
+
+// vulncheckRequest contains information passed
+// to a scan endpoint.
+type vulncheckRequest struct {
+	scan.ModuleURLPath
+	vulncheckRequestParams
+}
+
+// vulncheckRequestParams has query parameters for a vulncheck scan request.
+type vulncheckRequestParams struct {
+	ImportedBy int
+	Mode       string
+	Insecure   bool
+	// TODO: support optional parameters?
+}
+
+// These methods implement queue.Task.
+func (r *vulncheckRequest) Name() string { return r.Module + "@" + r.Version }
+
+func (r *vulncheckRequest) Path() string {
+	p := r.Module + "@" + r.Version
+	if r.Suffix != "" {
+		p += "/" + r.Suffix
+	}
+	return p
+}
+
+func (r *vulncheckRequest) Params() string {
+	return scan.FormatParams(r.vulncheckRequestParams)
+}
+
+// parseVulncheckRequest parses an http request r for an endpoint
+// prefix and produces a corresponding ScanRequest.
+//
+// The module and version should have one of the following three forms:
+//   - <module>/@v/<version>
+//   - <module>@<version>
+//   - <module>/@latest
+//
+// (These are the same forms that the module proxy accepts.)
+func parseVulncheckRequest(r *http.Request, prefix string) (*vulncheckRequest, error) {
+	mp, err := scan.ParseModuleURLPath(strings.TrimPrefix(r.URL.Path, prefix))
+	if err != nil {
+		return nil, err
+	}
+
+	rp := vulncheckRequestParams{ImportedBy: -1}
+	if err := scan.ParseParams(r, &rp); err != nil {
+		return nil, err
+	}
+	if rp.ImportedBy < 0 {
+		return nil, errors.New(`missing or negative "importedby" query param`)
+	}
+	return &vulncheckRequest{
+		ModuleURLPath:          mp,
+		vulncheckRequestParams: rp,
+	}, nil
+}