package main import ( "errors" "io" "slices" "strings" "testing" "testing/iotest" "time" ) // parseTimeLimit is how long a test waits for parseCSV to return before it // fails. const parseTimeLimit = 2 * time.Second // TestParseCSVReadsTodaysLayout parses an as.csv with the country code column // that the source added after the description. It expects each AS with its // handle and description spelled exactly as in the file, accents included. func TestParseCSVReadsTodaysLayout(t *testing.T) { t.Parallel() const asCSV = `asn,handle,description,country-code 1,LVLT-1,Level 3 Parent LLC,US 1251,FUNDACAO-AMPARO-PESQUISA,FUNDAÇÃO DE AMPARO À PESQUISA DO ESTADO SÃO PAULO,BR ` got, err := parseCSV(strings.NewReader(asCSV)) if err != nil { t.Fatalf("parseCSV: %v", err) } want := []ASInfo{ {ASN: 1, Handle: "LVLT-1", Description: "Level 3 Parent LLC"}, { ASN: 1251, Handle: "FUNDACAO-AMPARO-PESQUISA", Description: "FUNDAÇÃO DE AMPARO À PESQUISA DO ESTADO " + "SÃO PAULO", }, } if !slices.Equal(got, want) { t.Errorf("parseCSV = %+v, want %+v", got, want) } } // TestParseCSVRejectsAnotherHeader parses files whose first columns are not // the AS number, the handle and the description. It expects an error rather // than a list read from the wrong columns. func TestParseCSVRejectsAnotherHeader(t *testing.T) { t.Parallel() for _, asCSV := range []string{ "asn,handle\n1,LVLT-1\n", "asn,description,handle\n1,Level 3 Parent LLC,LVLT-1\n", } { _, err := parseCSV(strings.NewReader(asCSV)) if err == nil { t.Errorf("parseCSV(%q): no error", asCSV) } } } // TestParseCSVRejectsAFileWithNoASes parses files that hold no AS, one with // only the header and one whose only record has no valid AS number. It // expects errNoASes, so that make asupdate keeps the old list. func TestParseCSVRejectsAFileWithNoASes(t *testing.T) { t.Parallel() for _, asCSV := range []string{ "asn,handle,description,country-code\n", "asn,handle,description,country-code\nAS1,LVLT-1,Level 3,US\n", } { _, err := parseCSV(strings.NewReader(asCSV)) if !errors.Is(err, errNoASes) { t.Errorf("parseCSV(%q) = %v, want %v", asCSV, err, errNoASes) } } } // TestParseCSVSkipsARecordThatDoesNotParse parses an as.csv with a record // that has too few fields between two valid records. It expects both valid // ASes and no error, so that one bad record does not stop make asupdate. func TestParseCSVSkipsARecordThatDoesNotParse(t *testing.T) { t.Parallel() const asCSV = "asn,handle,description,country-code\n" + "1,LVLT-1,Level 3 Parent LLC,US\n" + "2,UDEL-DCN\n" + "3,MIT-GATEWAYS,Massachusetts Institute of Technology,US\n" got, err := parseCSV(strings.NewReader(asCSV)) if err != nil { t.Fatalf("parseCSV: %v", err) } want := []ASInfo{ {ASN: 1, Handle: "LVLT-1", Description: "Level 3 Parent LLC"}, { ASN: 3, Handle: "MIT-GATEWAYS", Description: "Massachusetts Institute of Technology", }, } if !slices.Equal(got, want) { t.Errorf("parseCSV = %+v, want %+v", got, want) } } // TestParseCSVStopsWhenTheDownloadBreaksOff parses an as.csv whose download // breaks off after the first record. From then on every read fails, as on a // broken connection. It expects parseCSV to return that error, so that make // asupdate keeps the old list. func TestParseCSVStopsWhenTheDownloadBreaksOff(t *testing.T) { t.Parallel() const asCSV = "asn,handle,description,country-code\n" + "1,LVLT-1,Level 3 Parent LLC,US\n" download := io.MultiReader( strings.NewReader(asCSV), iotest.ErrReader(io.ErrUnexpectedEOF)) // parseCSV runs on its own goroutine, so that a parseCSV that never // returns fails the test instead of hanging it. parsed := make(chan error, 1) go func() { _, err := parseCSV(download) parsed <- err }() select { case err := <-parsed: if !errors.Is(err, io.ErrUnexpectedEOF) { t.Errorf("parseCSV = %v, want %v", err, io.ErrUnexpectedEOF) } case <-time.After(parseTimeLimit): t.Fatalf("parseCSV did not return within %v", parseTimeLimit) } }