blob: 321792271f8ed995a10ada6006de319f6b044133 [file] [log] [blame]
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package report
import (
"errors"
"flag"
"fmt"
"os"
"path"
"path/filepath"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"golang.org/x/tools/txtar"
"golang.org/x/vulndb/internal/osv"
"golang.org/x/vulndb/internal/proxy"
"golang.org/x/vulndb/internal/test"
)
var (
realProxy = flag.Bool("proxy", false, "if true, contact the real module proxy and update expected responses")
)
var (
validStdLibReferences = []*Reference{
{Type: osv.ReferenceTypeFix, URL: "https://go.dev/cl/12345"},
{Type: osv.ReferenceTypeWeb, URL: "https://groups.google.com/g/golang-announce/c/12345"},
{Type: osv.ReferenceTypeReport, URL: "https://go.dev/issue/12345"},
}
validCVEMetadata = &CVEMeta{
ID: "CVE-0000-1111",
CWE: "CWE XXX: A CWE description",
}
noop = func(*Report) {}
)
func validReport(f func(r *Report)) Report {
r := Report{
ID: "GO-0000-0000",
Modules: []*Module{{
Module: "golang.org/x/net",
VulnerableAt: "1.2.3",
Packages: []*Package{{
Package: "golang.org/x/net/http2",
}},
}},
Description: "description",
Summary: "A summary",
CVEs: []string{"CVE-1234-0000"},
}
f(&r)
return r
}
func validStdReport(f func(r *Report)) Report {
r := Report{
ID: "GO-0000-0000",
Modules: []*Module{{
Module: "std",
VulnerableAt: "1.2.3",
Packages: []*Package{{
Package: "net/http",
}},
}},
Description: "description",
Summary: "A summary",
References: validStdLibReferences,
}
f(&r)
return r
}
func validExcludedReport(f func(r *Report)) Report {
r := Report{
ID: "GO-0000-0000",
Excluded: "NOT_GO_CODE",
CVEs: []string{"CVE-2022-1234545"},
}
f(&r)
return r
}
func TestLint(t *testing.T) {
pc, err := proxy.NewTestClient(t, *realProxy)
if err != nil {
t.Fatal(err)
}
for _, test := range []lintTC{
{
name: "module_version_ok",
desc: "Module-version pairs that exist are OK.",
report: validReport(func(r *Report) {
r.Modules = append(r.Modules, &Module{
Module: "golang.org/x/net",
Versions: []VersionRange{
{
Introduced: "0.2.0",
},
}})
}),
pc: pc,
// No lints.
},
{
name: "module_version_invalid",
desc: "Version@module must exist.",
report: validReport(func(r *Report) {
r.Modules = append(r.Modules, &Module{
Module: "golang.org/x/net",
Versions: []VersionRange{
{
Introduced: "0.2.5", // does not exist
},
}})
}),
pc: pc,
want: []string{`version 0.2.5 does not exist`},
wantNumLints: 1,
},
{
name: "module_non_canonical",
desc: "Module names must be canonical.",
report: validReport(func(r *Report) {
r.Modules = append(r.Modules, &Module{
Module: "github.com/golang/vuln",
Versions: []VersionRange{
{
Introduced: "0.1.0",
},
}})
}),
pc: pc,
want: []string{`module is not canonical`},
wantNumLints: 1,
},
{
name: "multiple_problems",
desc: "A test for a report with multiple module-version issues at once.",
report: validReport(func(r *Report) {
r.Modules = append(r.Modules, &Module{
Module: "github.com/golang/vuln",
Versions: []VersionRange{
{
Introduced: "0.1.0",
Fixed: "0.2.5", // does not exist
},
{
Introduced: "0.2.6", // does not exist
},
}})
}),
pc: pc,
want: []string{"2 versions do not exist: 0.2.5, 0.2.6 and module is not canonical"},
wantNumLints: 1,
},
{
name: "no_proxy_client",
desc: "A non-nil proxy client must be provided.",
report: validReport(noop),
pc: nil,
want: []string{"proxy client is nil"},
wantNumLints: 1,
},
} {
test := test
t.Run(test.name, func(t *testing.T) {
got := test.report.Lint(test.pc)
updateAndCheckGolden(t, &test, got)
checkLints(t, got, test.want)
})
}
}
// lintTC is a lint test case.
type lintTC struct {
name, desc string
report Report
pc *proxy.Client
want []string
wantNumLints int
}
func TestLintOffline(t *testing.T) {
for _, test := range []lintTC{
{
name: "no_ID",
desc: "All reports must have an ID.",
report: validReport(func(r *Report) {
r.ID = ""
}),
want: []string{"missing ID"},
wantNumLints: 1,
},
{
name: "no_modules",
desc: "All reports (except excluded reports marked NOT_GO_CODE) must have at least one module.",
report: validReport(func(r *Report) {
r.Modules = nil
}),
want: []string{"no modules"},
wantNumLints: 1,
},
{
name: "no_module_path",
desc: "Every module must have a path.",
report: validReport(func(r *Report) {
r.Modules[0].Module = ""
}),
want: []string{"modules[0]: missing module"},
wantNumLints: 1,
},
{
name: "no_advisory",
desc: "Reports without a description must have an advisory link.",
report: validReport(func(r *Report) {
r.Description = ""
r.References = nil
}),
want: []string{"missing advisory"},
wantNumLints: 1,
},
{
name: "no_description_ok",
desc: "Reports with no description are OK if they have an advisory.",
report: validReport(func(r *Report) {
r.Description = ""
r.References = []*Reference{
{Type: osv.ReferenceTypeAdvisory, URL: "https://example.com"},
}
}),
wantNumLints: 0,
},
{
name: "no_description_go_cve",
desc: "Reports with a CVE assigned by the Go CNA must have a description.",
report: validReport(func(r *Report) {
r.Description = ""
r.CVEs = nil
r.CVEMetadata = validCVEMetadata
}),
want: []string{"missing description"},
wantNumLints: 1,
},
{
name: "description_line_length",
desc: "Descriptions must not (except in special cases) contain lines longer than 80 characters.",
report: validReport(func(r *Report) {
r.Description = "This line is too long; it needs to be shortened to less than 80 characters to pass the lint check"
}),
want: []string{"description contains line > 80 characters long"},
wantNumLints: 1,
},
{
name: "description_long_word_ok",
desc: "Descriptions may contain lines longer than 80 characters if the line is a single word.",
report: validReport(func(r *Report) {
r.Description = "http://1234567890.abcdefghijklmnopqrstuvwxyz.1234567890.abcdefghijklmnopqrstuvwxyz" // 82 chars ok if single word
}),
// No lints.
},
{
name: "no_summary",
desc: "Regular (non-excluded) reports must have a summary.",
report: validReport(func(r *Report) {
r.Summary = ""
}),
want: []string{"missing summary"},
wantNumLints: 1,
},
{
name: "summary_todo",
desc: "Summaries must not contain TODOs.",
report: validReport(func(r *Report) {
r.Summary = "TODO: fill this out"
}),
want: []string{"summary contains a TODO"},
wantNumLints: 1,
},
{
name: "summary_too_long",
desc: "The summary must be 100 characters or less.",
report: validReport(func(r *Report) {
r.Summary = "This summary is too long; it needs to be shortened to less than 101 characters to pass the lint check"
}),
want: []string{"too long"},
wantNumLints: 1,
},
{
name: "summary_period",
desc: "The summary should not end in a period. It should be a phrase, not a sentence.",
report: validReport(func(r *Report) {
r.Summary = "This summary is a sentence, not a phrase."
}),
want: []string{"should not end in a period"},
wantNumLints: 1,
},
{
name: "no_package_path",
desc: "All packages must have a path.",
report: validReport(func(r *Report) {
r.Modules[0].Packages[0].Package = ""
}),
want: []string{"golang.org/x/net: missing package"},
wantNumLints: 1,
},
{
name: "no_vulnerable_at_or_skip_fix",
desc: "At least one of module.vulnerable_at and module.package.skip_fix must be set.",
report: validReport(func(r *Report) {
r.Modules[0].VulnerableAt = ""
r.Modules[0].Packages[0].SkipFix = ""
}),
want: []string{"missing skip_fix and vulnerable_at"},
wantNumLints: 1,
},
{
name: "skip_fix_ok",
desc: "The vulnerable_at field can be blank if skip_fix is set.",
report: validReport(func(r *Report) {
r.Modules[0].VulnerableAt = ""
r.Modules[0].Packages[0].SkipFix = "a reason"
}),
// No lints.
},
{
name: "vulnerable_at_and_skip_fix_ok",
desc: "It is OK to set both module.vulnerable_at and module.package.skip_fix.",
report: validReport(func(r *Report) {
r.Modules[0].VulnerableAt = "1.2.3"
r.Modules[0].Packages[0].SkipFix = "a reason"
}),
// No lints.
},
{
name: "vulnerable_at_out_of_range",
desc: "Field module.vulnerable_at must be inside the vulnerable version range for the module.",
report: validStdReport(func(r *Report) {
r.Modules[0].VulnerableAt = "2.0.0"
r.Modules[0].Versions = []VersionRange{
{Fixed: "1.2.1"},
}
}),
want: []string{"vulnerable_at version 2.0.0 is not inside vulnerable range"},
wantNumLints: 1,
},
{
name: "unsupported_versions",
desc: "The unsupported_versions field should never be set.",
report: validStdReport(func(r *Report) {
r.Modules[0].UnsupportedVersions = []UnsupportedVersion{
{Version: "1.2.1", Type: "unknown"},
}
}),
want: []string{"version issue: 1 unsupported version(s)"},
wantNumLints: 1,
},
{
name: "module_package_prefix",
desc: "In third party reports, module names must be prefixes of package names.",
report: validReport(func(r *Report) {
r.Modules[0].Module = "example.com/module"
r.Modules[0].Packages[0].Package = "example.com/package"
}),
want: []string{"module must be a prefix of package"},
wantNumLints: 1,
},
{
name: "invalid_package_path",
desc: "In third party reports, package paths must pass validity checks in x/mod/module.CheckImportPath.",
report: validReport(func(r *Report) {
r.Modules[0].Module = "invalid."
r.Modules[0].Packages[0].Package = "invalid."
}),
want: []string{"malformed import path"},
wantNumLints: 1,
},
{
name: "no_package_path_stdlib",
desc: "All packages must have a path.",
report: validStdReport(func(r *Report) {
r.Modules[0].Packages[0].Package = ""
}),
want: []string{"missing package"},
wantNumLints: 1,
},
{
name: "no_package_stdlib",
desc: "In standard library reports, all modules must contain at least one package.",
report: validStdReport(func(r *Report) {
r.Modules[0].Packages = nil
}),
want: []string{"missing package"},
wantNumLints: 1,
},
{
name: "wrong_module_cmd",
desc: "Packages beginning with 'cmd' should be in the 'cmd' module.",
report: validStdReport(func(r *Report) {
r.Modules[0].Module = "std"
r.Modules[0].Packages[0].Package = "cmd/go"
}),
want: []string{`should be in module "cmd", not "std"`},
wantNumLints: 1,
},
{
name: "versions_overlapping_ranges",
desc: "Version ranges must not overlap.",
report: validStdReport(func(r *Report) {
r.Modules[0].Versions = []VersionRange{
// Two fixed versions in a row with no introduced.
{Fixed: "1.2.1"}, {Fixed: "1.3.2"},
}
}),
want: []string{"introduced and fixed versions must alternate"},
wantNumLints: 1,
},
{
name: "versions_fixed_before_introduced",
desc: "Within a version range, the fixed version must come before the introduced version.",
report: validStdReport(func(r *Report) {
r.Modules[0].Versions = []VersionRange{
{
Introduced: "1.3.0",
Fixed: "1.2.1",
},
}
}),
want: []string{`range events must be in strictly ascending order (found 1.3.0>=1.2.1)`},
wantNumLints: 1,
},
{
name: "versions_checked_no_vulnerable_at",
desc: "Version checks still apply if vulnerable_at is not set.",
report: validStdReport(func(r *Report) {
r.Modules[0].VulnerableAt = ""
r.Modules[0].Versions = []VersionRange{
// Two fixed versions in a row with no introduced.
{Fixed: "1.2.1"}, {Fixed: "1.3.2"},
}
}),
want: []string{
"introduced and fixed versions must alternate",
"missing skip_fix and vulnerable_at",
},
wantNumLints: 2,
},
{
name: "invalid_semver",
desc: "All versions must be valid, unprefixed, semver",
report: validStdReport(func(r *Report) {
r.Modules[0].Versions = []VersionRange{
{
Introduced: "1.3.X",
},
}
}),
want: []string{`invalid or non-canonical semver version (found 1.3.X)`},
wantNumLints: 1,
},
{
name: "bad_cve",
desc: "All CVEs must be valid.",
report: validReport(func(r *Report) {
r.CVEs = []string{"CVE.1234.5678"}
}),
want: []string{"malformed cve identifier"},
wantNumLints: 1,
},
{
name: "bad_ghsa",
desc: "All GHSAs must be valid.",
report: validReport(func(r *Report) {
r.GHSAs = []string{"GHSA-123"}
}),
want: []string{"GHSA-123 is not a valid GHSA"},
wantNumLints: 1,
},
{
name: "cve_and_cve_metadata_ok",
desc: "It is OK to set both cves and cve_metadata.",
report: validReport(func(r *Report) {
r.CVEs = []string{"CVE-0000-1111"}
r.CVEMetadata = validCVEMetadata
}),
// No lints.
},
{
name: "cve_metadata_missing_fields",
desc: "Field cve_metadata (if not nil), must have an ID and CWE.",
report: validReport(func(r *Report) {
r.CVEs = nil
r.CVEMetadata = &CVEMeta{
// missing fields
}
}),
want: []string{"cve_metadata.id is required", "cve_metadata.cwe is required"},
wantNumLints: 2,
},
{
name: "cve_metadata_bad_fields",
desc: "Field cve_metadata must contain valid entries for ID and CWE.",
report: validReport(func(r *Report) {
r.CVEs = nil
r.CVEMetadata = &CVEMeta{
ID: "CVE.0000.1111",
CWE: "TODO",
}
}),
want: []string{"malformed cve_metadata.id identifier", "cve_metadata.cwe contains a TODO"},
wantNumLints: 2,
},
{
name: "reference_invalid_type",
desc: "Reference type must be one of the pre-defined types in osv.ReferenceTypes.",
report: validReport(func(r *Report) {
r.References = append(r.References, &Reference{
Type: "INVALID",
URL: "http://go.dev/",
})
}),
want: []string{"not a valid reference type"},
wantNumLints: 1,
},
{
name: "references_multiple_advisories",
desc: "Each report should contain at most one advisory reference.",
report: validReport(func(r *Report) {
r.References = append(r.References, &Reference{
Type: "ADVISORY",
URL: "http://go.dev/a",
}, &Reference{
Type: "ADVISORY",
URL: "http://go.dev/b",
})
}),
want: []string{"at most one advisory link"},
wantNumLints: 1,
},
{
name: "references_redundant_web_advisories",
desc: "Reports should not contain redundant web-type references linking to CVEs/GHSAs listed in the cves/ghsas sections.",
report: validReport(func(r *Report) {
r.CVEs = []string{"CVE-0000-0000", "CVE-0000-0001"}
r.GHSAs = []string{"GHSA-0000-0000-0000"}
r.References = append(r.References, &Reference{
Type: "WEB",
URL: "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-0000-0000",
}, &Reference{
Type: "WEB",
URL: "https://nvd.nist.gov/vuln/detail/CVE-0000-0001",
}, &Reference{
Type: "WEB",
URL: "https://nvd.nist.gov/vuln/detail/CVE-0000-0002", // ok
}, &Reference{
Type: "WEB",
URL: "https://github.com/advisories/GHSA-0000-0000-0000",
}, &Reference{
Type: "WEB",
URL: "https://github.com/advisories/GHSA-0000-0000-0001", // ok
})
}),
want: []string{
"redundant non-advisory reference to CVE-0000-0000",
"redundant non-advisory reference to CVE-0000-0001",
"redundant non-advisory reference to GHSA-0000-0000-0000",
},
wantNumLints: 3,
},
{
name: "references_unfixed",
desc: "References should not contain non-canonical link formats (that can be auto-fixed).",
report: validReport(func(r *Report) {
r.References = []*Reference{
{Type: osv.ReferenceTypeFix, URL: "https://github.com/golang/go/commit/12345"},
{Type: osv.ReferenceTypeReport, URL: "https://github.com/golang/go/issues/12345"},
{Type: osv.ReferenceTypeWeb, URL: "https://golang.org/xxx"},
{Type: osv.ReferenceTypeWeb, URL: "https://groups.google.com/forum/#!/golang-announce/12345/1/"},
}
}),
want: []string{
`"https://github.com/golang/go/issues/12345" should be "https://go.dev/issue/12345"`,
`"https://golang.org/xxx" should be "https://go.dev/xxx"`,
`"https://github.com/golang/go/commit/12345" should be "https://go.googlesource.com/+/12345"`,
`"https://groups.google.com/forum/#!/golang-announce/12345/1/" should be "https://groups.google.com/g/golang-announce/c/12345/m/1/"`},
wantNumLints: 4,
},
{
name: "references_incorrect_stdlib",
desc: "Standard library reports must contain references matching a specific format.",
report: validStdReport(func(r *Report) {
r.References = []*Reference{
{Type: osv.ReferenceTypeAdvisory, URL: "http://www.example.com"},
{Type: osv.ReferenceTypeFix, URL: "https://go-review.googlesource.com/c/go/+/12345"},
{Type: osv.ReferenceTypeFix, URL: "https://github.com/golang/go/commit/12345"},
{Type: osv.ReferenceTypeReport, URL: "https://github.com/golang/go/issues/12345"},
{Type: osv.ReferenceTypeWeb, URL: "https://go.dev/"},
// no announce link
}
}),
want: []string{
// Standard library specific errors.
"advisory reference should not be set",
"fix reference should match",
"report reference should match",
"references should contain an announcement link",
"web references should only contain announcement links",
// Unfixed link errors.
`"https://github.com/golang/go/commit/12345" should be "https://go.googlesource.com/+/12345"`,
`"https://github.com/golang/go/issues/12345" should be "https://go.dev/issue/12345"`,
},
wantNumLints: 8,
},
{
name: "references_missing_stdlib",
desc: "Standard library reports must contain at least one report, fix, and announcement link.",
report: validStdReport(func(r *Report) {
r.References = []*Reference{
// no links
}
}),
want: []string{
"references should contain at least one report",
"references should contain at least one fix",
"references should contain an announcement link",
},
wantNumLints: 3,
},
{
name: "reference_invalid_URL",
desc: "References must be valid URLs.",
report: validReport(func(r *Report) {
r.References = []*Reference{
{
Type: osv.ReferenceTypeFix,
URL: "go.dev/cl/12345", // needs "https://" prefix
},
}
}),
want: []string{
`"go.dev/cl/12345" is not a valid URL`,
},
wantNumLints: 1,
},
{
name: "missing_fields_excluded",
desc: "Excluded reports must contain (at least): a valid excluded reason, a module, and one CVE or GHSA.",
report: validExcludedReport(func(r *Report) {
r.Excluded = "not a real reason"
r.Modules = nil
r.CVEs = nil
r.GHSAs = nil
}),
want: []string{
"not a valid excluded reason",
"no modules",
"excluded report must have at least one associated CVE or GHSA",
},
wantNumLints: 3,
},
{
name: "bad_related",
desc: "The related field must not contain duplicate or invalid IDs.",
report: validReport(func(r *Report) {
r.CVEs = []string{"CVE-0000-1111"}
r.Related = []string{
"not-an-id", // bad
"CVE-0000-1111", // bad (duplicate)
"CVE-0000-1112", // ok
"GHSA-0000-0000-0000", // ok
"GO-1990-0001", // ok
}
}),
want: []string{
"not-an-id is not a recognized identifier",
"CVE-0000-1111 is also listed among aliases",
},
wantNumLints: 2,
},
{
name: "module_version_offline",
desc: "In offline mode, module-version consistency is not checked because it requires a call to the module proxy.",
report: validReport(func(r *Report) {
r.Modules = append(r.Modules, &Module{
Module: "golang.org/x/net",
Versions: []VersionRange{
{
Introduced: "0.2.5", // does not exist
},
}})
}),
// No lints: in offline mode, versions aren't checked.
},
{
name: "valid_excluded",
desc: "No lints are generated for valid excluded reports.",
report: validExcludedReport(noop),
// No lints.
},
} {
test := test
t.Run(test.name, func(t *testing.T) {
got := test.report.LintOffline()
updateAndCheckGolden(t, &test, got)
})
}
}
// The name of the "file" in the txtar archive containing the expected output.
const golden = "golden"
func updateAndCheckGolden(t *testing.T, test *lintTC, lints []string) {
if *updateGolden {
if err := updateGoldenFile(t, test, lints); err != nil {
t.Error(err)
}
}
checkGoldenFile(t, test, lints)
}
func updateGoldenFile(t *testing.T, tc *lintTC, lints []string) error {
t.Helper()
fpath := goldenFilename(t)
// Double-check that we got the right number of lints, to make it
// harder to lose/gain a lint with the auto-update.
if tc.wantNumLints != len(lints) {
return fmt.Errorf("%s: cannot update: got %d lints, want %d", fpath, len(lints), tc.wantNumLints)
}
rb, err := reportToBytes(&tc.report)
if err != nil {
return err
}
return test.WriteTxtar(fpath, []txtar.File{
{
Name: testYAMLFilename(&tc.report),
Data: rb,
},
{
Name: golden,
Data: lintsToBytes(lints),
},
}, newComment(t, tc))
}
func checkGoldenFile(t *testing.T, tc *lintTC, lints []string) {
t.Helper()
fpath := goldenFilename(t)
if _, err := os.Stat(fpath); err != nil {
t.Errorf("golden file %s does not exist (re-run test with -update flag)", fpath)
return
}
ar, err := txtar.ParseFile(fpath)
if err != nil {
t.Error(err)
return
}
wantComment, gotComment := newComment(t, tc), string(ar.Comment)
if err := test.CheckComment(wantComment, gotComment); err != nil {
t.Error(err)
}
// Check that all expected files are present and have the correct contents.
reportFile := testYAMLFilename(&tc.report)
foundReport, foundGolden := false, false
for _, f := range ar.Files {
switch af := f.Name; {
case af == golden:
want := f.Data
got := lintsToBytes(lints)
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("%s: %s: mismatch (-want, +got):\n%s", fpath, af, diff)
}
foundGolden = true
case af == reportFile:
want := f.Data
got, err := reportToBytes(&tc.report)
if err != nil {
t.Errorf("%s: %s", af, err)
continue
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("%s: %s: mismatch (-want, +got):\n%s", fpath, af, diff)
}
foundReport = true
default:
t.Errorf("%s: unexpected archive file %s, expected one of (%q, %q)", fpath, af, reportFile, golden)
}
}
if !foundReport {
t.Errorf("%s: no report found (want archive file %q)", fpath, reportFile)
}
if !foundGolden {
t.Errorf("%s: no golden found (want archive file %q)", fpath, golden)
}
}
func testYAMLFilename(r *Report) string {
id := r.ID
if id == "" {
id = "NO-GO-ID"
}
// Use path instead of filepath so that the paths
// always use forward slashes.
// These filenames are only used inside .txtar archives.
return path.Join(dataFolder, r.folder(), id+".yaml")
}
func newComment(t *testing.T, tc *lintTC) string {
t.Helper()
return fmt.Sprintf("Test: %s\nDescription: %s", t.Name(), tc.desc)
}
func goldenFilename(t *testing.T) string {
t.Helper()
return filepath.Join("testdata", "lint", t.Name()+".txtar")
}
func lintsToBytes(lints []string) []byte {
return []byte(strings.Join(lints, "\n") + "\n")
}
func reportToBytes(report *Report) ([]byte, error) {
ys, err := report.ToString()
if err != nil {
return nil, err
}
return []byte(ys + "\n"), nil
}
func checkLints(t *testing.T, got, want []string) {
var missing []string
for _, w := range want {
found := false
for _, g := range got {
if strings.Contains(g, w) {
found = true
continue
}
}
if !found {
missing = append(missing, w)
}
}
if len(missing) > 0 {
t.Errorf("missing expected lint errors in report:\n"+
"got: %q\n"+
"want: %q\n", got, missing)
}
// Check for unexpected lint errors if there are no missing ones.
if len(missing) == 0 {
var unexpected []string
for _, g := range got {
found := false
for _, w := range want {
if strings.Contains(g, w) {
found = true
continue
}
}
if !found {
unexpected = append(unexpected, g)
}
}
if len(unexpected) > 0 {
t.Errorf("unexpected lint errors in report:\n"+
"got: %q\n", unexpected)
}
}
}
func TestCheckFilename(t *testing.T) {
for _, test := range []struct {
desc string
filename string
report Report
wantErr error
}{
{
desc: "wrong ID",
filename: "data/reports/GO-0000-0000.yaml",
report: validReport(
func(r *Report) {
r.ID = "GO-0000-1111"
}),
wantErr: errWrongID,
},
{
desc: "excluded in correct directory",
filename: "data/excluded/GO-0000-0000.yaml",
report: validExcludedReport(noop),
wantErr: nil,
},
{
desc: "excluded in wrong directory",
filename: "data/wrong/GO-0000-0000.yaml",
report: validExcludedReport(noop),
wantErr: errWrongDir,
},
{
desc: "non-excluded in correct directory",
filename: "data/reports/GO-0000-0000.yaml",
report: validReport(noop),
wantErr: nil,
},
{
desc: "non-excluded in wrong directory",
filename: "data/wrong/GO-0000-0000.yaml",
report: validReport(noop),
wantErr: errWrongDir,
},
} {
test := test
t.Run(test.desc, func(t *testing.T) {
if err := test.report.CheckFilename(test.filename); !errors.Is(err, test.wantErr) {
t.Errorf("CheckFilename(%s) = %v, want error %v", test.filename, err, test.wantErr)
}
})
}
}
func TestLintAsNotes(t *testing.T) {
// A report with lints.
report := validReport(
func(r *Report) {
r.Summary = ""
r.Notes = []*Note{
{Body: "an existing lint that will be deleted", Type: NoteTypeLint},
{Body: "a note added by a human", Type: NoteTypeNone}}
},
)
found := report.LintAsNotes(nil)
if !found {
t.Error("LintAsNotes() = false, want true")
}
want, got := []*Note{
{Body: "a note added by a human", Type: NoteTypeNone}, // preserved
{Body: "missing summary", Type: NoteTypeLint},
{Body: "proxy client is nil; cannot perform all lint checks", Type: NoteTypeLint}}, report.Notes
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want, +got):\n%s", diff)
}
}