func Reverse(s string) (string, error) {
if !utf8.ValidString(s) {
return s, errors.New("input is not valid UTF-8")
}
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r), nil
}
package main
import (
"testing"
"unicode/utf8"
)
func FuzzReverse(f testing.F) {
testcases := []string{"Hello, world", " ", "!12345"}
for _, tc := range testcases {
f.Add(tc) // Use f.Add to provide a seed corpus
}
f.Fuzz(func(t testing.T, orig string) {
rev, err1 := Reverse(orig)
if err1 != nil {
return
}
doubleRev, err2 := Reverse(rev)
if err2 != nil {
return
}
if orig != doubleRev {
t.Errorf("Before: %q, after: %q", orig, doubleRev)
}
if utf8.ValidString(orig) && !utf8.ValidString(rev) {
t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
}
})
}
└> go test -v -fuzz . --fuzztime=30s
=== FUZZ FuzzReverse
fuzz: elapsed: 0s, gathering baseline coverage: 0/47 completed
fuzz: elapsed: 0s, gathering baseline coverage: 47/47 completed, now fuzzing with 12 workers
fuzz: elapsed: 3s, execs: 697351 (232449/sec), new interesting: 0 (total: 47)
fuzz: elapsed: 6s, execs: 1448115 (250179/sec), new interesting: 0 (total: 47)
fuzz: elapsed: 9s, execs: 2151568 (234515/sec), new interesting: 0 (total: 47)
fuzz: elapsed: 12s, execs: 2837852 (228799/sec), new interesting: 0 (total: 47)
fuzz: elapsed: 15s, execs: 3516539 (226187/sec), new interesting: 1 (total: 48)
fuzz: elapsed: 18s, execs: 4197205 (226882/sec), new interesting: 1 (total: 48)
fuzz: elapsed: 21s, execs: 4859241 (220710/sec), new interesting: 1 (total: 48)
fuzz: elapsed: 24s, execs: 5493189 (211323/sec), new interesting: 1 (total: 48)
fuzz: elapsed: 27s, execs: 6156103 (220938/sec), new interesting: 2 (total: 49)
fuzz: elapsed: 30s, execs: 6827045 (223682/sec), new interesting: 3 (total: 50)
fuzz: elapsed: 30s, execs: 6827045 (0/sec), new interesting: 3 (total: 50)
--- PASS: FuzzReverse (30.09s)
PASS
ok github.com/ckalpakoglu/fuzzing 30.094s
├── cmd
│ └── server.go
├── handlers
│ ├── db.go
│ ├── handlers.go
│ ├── user.go
│ └── user_test.go
├── main.go
└── util
└── util.go
package cmd
import (
"database/sql"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
_ "github.com/mattn/go-sqlite3"
"github.com/kondukto-io/simple-fuzzing/handlers"
)
const (
port = ":8888"
)
func Execute() error {
// setup the database
db, err := sql.Open("sqlite3", "file::memory:?cache=shared")
if err != nil {
panic(err)
}
defer db.Close()
e := echo.New()
// middlewares
e.Use(middleware.Logger())
// run the db migration. This should run once
err = handlers.MigrateDB(db)
if err != nil {
panic(err)
}
// Initialize the handlers
h := handlers.NewHandler(db)
// Routes
e.POST("/create", h.CreateUser)
e.GET("/user/:id", h.GetUserByID)
return e.Start(port)
}
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/kondukto-io/simple-fuzzing/util"
)
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func (h *Handler) CreateUser(c echo.Context) error {
u := new(User)
if err := c.Bind(u); err != nil {
/ in the production you should not dump the error message directly
return &echo.HTTPError{Code: http.StatusBadRequest, Message: err.Error()}
}
stmt, err := h.db.Prepare("INSERT INTO users(id, name, email) values (?, ?, ?)")
if err != nil {
// in ,the production you should not dump the error message directly
return &echo.HTTPError{Code: http.StatusBadRequest, Message: err.Error()}
}
defer stmt.Close()
_, err = stmt.Exec(u.ID, u.Name, u.Email)
if err != nil {
// in the production you should not dump all the error message
return &echo.HTTPError{Code: http.StatusBadRequest, Message: err.Error()}
}
return c.JSON(http.StatusOK, u)
}
//...<snipped>...
package handlers
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"regexp"
"testing"
"unicode/utf8"
"github.com/DATA-DOG/go-sqlmock"
"github.com/labstack/echo/v4"
"github.com/kondukto-io/simple-fuzzing/util"
)
var (
// we use test cases for the unit tests
// and for fuzz test as a seed corpus
tests = []struct {
name string
args User
wantErr bool
}{
{
name: "success",
args: User{
ID: "1111",
Name: "kondukto",
Email: "helo@kondukto.io",
},
wantErr: false,
},
{
name: "fail",
args: User{
ID: "1212121212121212121212121111",
Name: "kondukto",
Email: "helo@kondukto.io",
},
wantErr: true,
},
{
name: "fail",
args: User{
ID: "s1111", // not a valid ID
Name: "kondukto",
Email: "helo@kondukto.io",
},
wantErr: true,
},
}
)
func FuzzCreateUser(f *testing.F) {
// setup the db
db, mock, err := sqlmock.New()
if err != nil {
f.Fatalf("an error '%s' was not expected when opening a mock db conn", err)
}
defer db.Close()
for _, tt := range tests {
f.Add(tt.args.ID, tt.args.Name, tt.args.Email)
}
f.Fuzz(func(t *testing.T, id, name, email string) {
if !util.VaildID(id) || !utf8.ValidString(name) || !utf8.ValidString(email) {
return
}
mock.ExpectPrepare(regexp.QuoteMeta("INTO users(id, name, email) values (?, ?, ?)"))
h := NewHandler(db)
input := User{
ID: id,
Name: name,
Email: email,
}
t.Log(input)
body, err := json.Marshal(input)
if err != nil {
t.Fatalf("error %v", err)
}
e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetPath("/create")
mock.ExpectExec(regexp.QuoteMeta("INSERT INTO users(id, name, email) values (?, ?, ?)")).
WithArgs(input.ID, input.Name, input.Email).WillReturnResult(sqlmock.NewResult(1, 1))
// testing the function
if err := h.CreateUser(c); err != nil {
t.Errorf("CreateUser() err = %v", err)
}
// ensure all expectations have been met
if err = mock.ExpectationsWereMet(); err != nil {
fmt.Printf("unmet expectation error: %s", err)
}
})
}
└> go test -v -fuzz=FuzzCreateUser --fuzztime=10s .
=== RUN TestCreateUser
=== RUN TestCreateUser/success
=== RUN TestCreateUser/fail
=== RUN TestCreateUser/fail#01
--- PASS: TestCreateUser (0.00s)
--- PASS: TestCreateUser/success (0.00s)
--- PASS: TestCreateUser/fail (0.00s)
--- PASS: TestCreateUser/fail#01 (0.00s)
=== RUN TestGetUserByID
=== RUN TestGetUserByID/success
=== PAUSE TestGetUserByID/success
=== RUN TestGetUserByID/fail
=== PAUSE TestGetUserByID/fail
=== RUN TestGetUserByID/fail#01
=== PAUSE TestGetUserByID/fail#01
=== CONT TestGetUserByID/success
=== CONT TestGetUserByID/fail#01
=== CONT TestGetUserByID/fail
--- PASS: TestGetUserByID (0.00s)
--- PASS: TestGetUserByID/fail#01 (0.00s)
--- PASS: TestGetUserByID/fail (0.00s)
--- PASS: TestGetUserByID/success (0.00s)
=== RUN FuzzGetUserByID
=== RUN FuzzGetUserByID/seed#0
user_test.go:179: ==== value is: 1111
=== RUN FuzzGetUserByID/seed#1
=== RUN FuzzGetUserByID/seed#2
--- PASS: FuzzGetUserByID (0.00s)
--- PASS: FuzzGetUserByID/seed#0 (0.00s)
--- PASS: FuzzGetUserByID/seed#1 (0.00s)
--- PASS: FuzzGetUserByID/seed#2 (0.00s)
=== FUZZ FuzzCreateUser
fuzz: elapsed: 0s, gathering baseline coverage: 0/168 completed
fuzz: elapsed: 0s, gathering baseline coverage: 168/168 completed, now fuzzing with 12 workers
fuzz: elapsed: 3s, execs: 66723 (22240/sec), new interesting: 6 (total: 174)
fuzz: elapsed: 6s, execs: 112971 (15416/sec), new interesting: 7 (total: 175)
fuzz: elapsed: 9s, execs: 134377 (7133/sec), new interesting: 7 (total: 175)
fuzz: elapsed: 11s, execs: 141507 (3532/sec), new interesting: 7 (total: 175)
--- PASS: FuzzCreateUser (11.03s)
PASS
ok github.com/kondukto-io/simple-fuzzing/handlers 11.036s
name: My workflow
# Controls when the action will run.
on:
push:
branches: [ master ]
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: '1.19'
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...
- name: Fuzz Create User handler
run: go test -v -fuzz=FuzzCreateUser --fuzztime=20s ./handlers
- name: Fuzz GetUserByID handler
run: go test -v -fuzz=FuzzGetUserByID --fuzztime=20s ./handlers