package parser import ( "crypto/sha1" "errors" "fmt" "strings" "testing" ) const validSolvedPuzzle = "123456789456789123789123456234567891567891234891234567345678912678912345912345678" func bankRecord(digits string, rating float64) string { hash := fmt.Sprintf("%x", sha1.Sum([]byte(digits)))[:12] return fmt.Sprintf("%s %s %.1f", hash, digits, rating) } func TestPuzzleBankParsesLFCRLFAndFinalLine(t *testing.T) { first := "0" + validSolvedPuzzle[1:] data := bankRecord(first, 1.2) + "\r\n\n" + bankRecord(validSolvedPuzzle, 2.5) parser := &PuzzleBank{} if err := parser.Parse([]byte(data)); err != nil { t.Fatalf("Parse() error = %v", err) } if got := len(parser.GetAllFields()); got != 2 { t.Fatalf("field count = %d, want 2", got) } second, err := parser.GetField(1) if err != nil { t.Fatal(err) } if second.GetRating() != 2.5 || !second.IsSolved() { t.Fatalf("second field: rating=%v solved=%v", second.GetRating(), second.IsSolved()) } } func TestPuzzleBankRejectsMalformedInput(t *testing.T) { valid := bankRecord(validSolvedPuzzle, 1.2) tests := map[string]string{ "empty": "\n\r\n", "columns": "broken", "hash length": "abc " + validSolvedPuzzle + " 1.2", "hash mismatch": "ffffffffffff" + valid[12:], "bad puzzle": bankRecord("x"+validSolvedPuzzle[1:], 1.2), "bad rating": strings.TrimSuffix(valid, "1.2") + "nope", } for name, input := range tests { t.Run(name, func(t *testing.T) { if err := (&PuzzleBank{}).Parse([]byte(input)); err == nil || (name != "empty" && !errors.Is(err, ErrInvalidInput)) { t.Fatalf("Parse() error = %v", err) } }) } } func TestPuzzleBankParseIsAtomicAndDoesNotAppend(t *testing.T) { parser := &PuzzleBank{} valid := bankRecord(validSolvedPuzzle, 1.2) if err := parser.Parse([]byte(valid)); err != nil { t.Fatal(err) } original, _ := parser.GetField(0) if err := parser.Parse([]byte(valid + "\ninvalid")); err == nil { t.Fatal("invalid Parse() error = nil") } afterFailure, _ := parser.GetField(0) if afterFailure != original || len(parser.GetAllFields()) != 1 { t.Fatal("failed parse changed previously parsed fields") } if err := parser.Parse([]byte(valid)); err != nil { t.Fatal(err) } if len(parser.GetAllFields()) != 1 { t.Fatal("repeated Parse() appended fields") } } func TestParserHelperBoundsAndDefensiveSlice(t *testing.T) { parser := &PuzzleString{} if err := parser.Parse([]byte(validSolvedPuzzle)); err != nil { t.Fatal(err) } for _, index := range []int{-1, 1} { if _, err := parser.GetField(index); !errors.Is(err, ErrFieldIndex) { t.Fatalf("GetField(%d) error = %v", index, err) } } fields := parser.GetAllFields() fields[0] = nil if stored, err := parser.GetField(0); err != nil || stored == nil { t.Fatal("GetAllFields exposed its backing slice") } } func TestPuzzleStringValidatesInput(t *testing.T) { parser := &PuzzleString{} if err := parser.Parse([]byte(" \n" + validSolvedPuzzle + "\n")); err != nil { t.Fatalf("Parse() error = %v", err) } if fields := parser.GetAllFields(); len(fields) != 1 || !fields[0].IsSolved() { t.Fatal("valid puzzle string was not parsed") } invalid := []string{"", validSolvedPuzzle[:80], "x" + validSolvedPuzzle[1:], "11" + validSolvedPuzzle[2:]} for _, input := range invalid { if err := (&PuzzleString{}).Parse([]byte(input)); err == nil { t.Fatalf("Parse(%q) error = nil", input) } } }