mirror of
https://gitlab.com/djdietrick/docs
synced 2026-05-03 00:20:54 -04:00
Added go http and db pages
This commit is contained in:
555
docs/go/advanced/db.md
Normal file
555
docs/go/advanced/db.md
Normal file
@@ -0,0 +1,555 @@
|
||||
# Databases
|
||||
|
||||
## Postgres
|
||||
|
||||
```bash
|
||||
go get github.com/jackc/pgconn
|
||||
go get github.com/jackc/pgx/v4
|
||||
go get github.com/jackc/pgx/v4/stdlib
|
||||
```
|
||||
|
||||
```go
|
||||
// models.go
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const dbTimeout = time.Second * 3
|
||||
|
||||
var db *sql.DB
|
||||
|
||||
type PostgresRepository struct {
|
||||
Conn *sql.DB
|
||||
}
|
||||
|
||||
func NewPostgresRepository(conn *sql.DB) *PostgresRepository {
|
||||
db = conn
|
||||
return &PostgresRepository{
|
||||
Conn: conn,
|
||||
}
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID int `json:"id"`
|
||||
Email string `json:"email"`
|
||||
FirstName string `json:"first_name,omitempty"`
|
||||
LastName string `json:"last_name,omitempty"`
|
||||
Password string `json:"-"`
|
||||
Active int `json:"active"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// GetAll returns a slice of all users, sorted by last name
|
||||
func (u *PostgresRepository) GetAll() ([]*User, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
|
||||
defer cancel()
|
||||
|
||||
query := `select id, email, first_name, last_name, password, user_active, created_at, updated_at
|
||||
from users order by last_name`
|
||||
|
||||
rows, err := u.Conn.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var users []*User
|
||||
|
||||
for rows.Next() {
|
||||
var user User
|
||||
err := rows.Scan(
|
||||
&user.ID,
|
||||
&user.Email,
|
||||
&user.FirstName,
|
||||
&user.LastName,
|
||||
&user.Password,
|
||||
&user.Active,
|
||||
&user.CreatedAt,
|
||||
&user.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
log.Println("Error scanning", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users = append(users, &user)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// GetByEmail returns one user by email
|
||||
func (u *PostgresRepository) GetByEmail(email string) (*User, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
|
||||
defer cancel()
|
||||
|
||||
query := `select id, email, first_name, last_name, password, user_active, created_at, updated_at from users where email = $1`
|
||||
|
||||
var user User
|
||||
row := db.QueryRowContext(ctx, query, email)
|
||||
|
||||
err := row.Scan(
|
||||
&user.ID,
|
||||
&user.Email,
|
||||
&user.FirstName,
|
||||
&user.LastName,
|
||||
&user.Password,
|
||||
&user.Active,
|
||||
&user.CreatedAt,
|
||||
&user.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetOne returns one user by id
|
||||
func (u *PostgresRepository) GetOne(id int) (*User, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
|
||||
defer cancel()
|
||||
|
||||
query := `select id, email, first_name, last_name, password, user_active, created_at, updated_at from users where id = $1`
|
||||
|
||||
var user User
|
||||
row := db.QueryRowContext(ctx, query, id)
|
||||
|
||||
err := row.Scan(
|
||||
&user.ID,
|
||||
&user.Email,
|
||||
&user.FirstName,
|
||||
&user.LastName,
|
||||
&user.Password,
|
||||
&user.Active,
|
||||
&user.CreatedAt,
|
||||
&user.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// Update updates one user in the database, using the information
|
||||
// stored in the receiver u
|
||||
func (u *PostgresRepository) Update(user User) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
|
||||
defer cancel()
|
||||
|
||||
stmt := `update users set
|
||||
email = $1,
|
||||
first_name = $2,
|
||||
last_name = $3,
|
||||
user_active = $4,
|
||||
updated_at = $5
|
||||
where id = $6
|
||||
`
|
||||
|
||||
_, err := db.ExecContext(ctx, stmt,
|
||||
user.Email,
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.Active,
|
||||
time.Now(),
|
||||
user.ID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteByID deletes one user from the database, by ID
|
||||
func (u *PostgresRepository) DeleteByID(id int) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
|
||||
defer cancel()
|
||||
|
||||
stmt := `delete from users where id = $1`
|
||||
|
||||
_, err := db.ExecContext(ctx, stmt, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Insert inserts a new user into the database, and returns the ID of the newly inserted row
|
||||
func (u *PostgresRepository) Insert(user User) (int, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
|
||||
defer cancel()
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), 12)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var newID int
|
||||
stmt := `insert into users (email, first_name, last_name, password, user_active, created_at, updated_at)
|
||||
values ($1, $2, $3, $4, $5, $6, $7) returning id`
|
||||
|
||||
err = db.QueryRowContext(ctx, stmt,
|
||||
user.Email,
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
hashedPassword,
|
||||
user.Active,
|
||||
time.Now(),
|
||||
time.Now(),
|
||||
).Scan(&newID)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return newID, nil
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// main.go, instantiate connection and repo
|
||||
package main
|
||||
|
||||
import (
|
||||
"authentication/data"
|
||||
|
||||
_ "github.com/jackc/pgconn"
|
||||
_ "github.com/jackc/pgx/v4"
|
||||
_ "github.com/jackc/pgx/v4/stdlib"
|
||||
)
|
||||
func connectToDB() *sql.DB {
|
||||
dsn := os.Getenv("DSN")
|
||||
|
||||
for {
|
||||
conn, err := openDB(dsn)
|
||||
if err != nil {
|
||||
log.Println("DB not ready yet...")
|
||||
counts++
|
||||
} else {
|
||||
log.Println("Connected to DB!")
|
||||
return conn
|
||||
}
|
||||
if counts > 10 {
|
||||
log.Println(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Println("Backing off for 2 seconds...")
|
||||
time.Sleep(time.Second * 2)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
func (app *Config) setupRepo(conn *sql.DB) {
|
||||
db := data.NewPostgresRepository(conn)
|
||||
app.Repo = db
|
||||
}
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
It is best to use a repository pattern when dealing with databases, because it allows you to implement a mock repo that implements all the same functions as the real repo with mock return values.
|
||||
|
||||
```go
|
||||
// repository.go
|
||||
package data
|
||||
|
||||
type Repository interface {
|
||||
GetAll() ([]*User, error)
|
||||
GetByEmail(email string) (*User, error)
|
||||
GetOne(id int) (*User, error)
|
||||
Insert(user User) (int, error)
|
||||
Update(user User) error
|
||||
DeleteByID(id int) error
|
||||
ResetPassword(password string, user User) error
|
||||
PasswordMatches(password string, user User) (bool, error)
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
package data
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PostgresTestRepository struct {
|
||||
Conn *sql.DB
|
||||
}
|
||||
|
||||
func NewPostgresTestRepository(conn *sql.DB) *PostgresTestRepository {
|
||||
return &PostgresTestRepository{
|
||||
Conn: conn,
|
||||
}
|
||||
}
|
||||
|
||||
// Implement all methods of Repository interface
|
||||
func (repo *PostgresTestRepository) GetAll() ([]*User, error) {
|
||||
users := []*User{}
|
||||
return users, nil
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// setup_test.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"authentication/data"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var testApp Config
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
repo := data.NewPostgresTestRepository(nil)
|
||||
testApp.Repo = repo
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
```
|
||||
|
||||
## Mongo
|
||||
|
||||
```bash
|
||||
go get go.mongodb.org/mongo-driver/mongo
|
||||
go get go.mongodb.org/mongo-driver/mongo/options
|
||||
```
|
||||
|
||||
```go
|
||||
// models.go
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
var client *mongo.Client
|
||||
|
||||
func New(mongo *mongo.Client) Models {
|
||||
client = mongo
|
||||
|
||||
return Models{
|
||||
LogEntry: LogEntry{},
|
||||
}
|
||||
}
|
||||
|
||||
type Models struct {
|
||||
LogEntry LogEntry
|
||||
}
|
||||
|
||||
type LogEntry struct {
|
||||
ID string `bson:"_id,omitempty" json:"id,omitempty"`
|
||||
Name string `bson:"name" json:"name"`
|
||||
Data string `bson:"data" json:"data"`
|
||||
CreatedAt time.Time `bson:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (l *LogEntry) Insert(entry LogEntry) error {
|
||||
collection := client.Database("logs").Collection("logs")
|
||||
|
||||
_, err := collection.InsertOne(context.TODO(), LogEntry{
|
||||
Name: entry.Name,
|
||||
Data: entry.Data,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Println("Error insertint into logs: ", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LogEntry) All() ([]*LogEntry, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
collection := client.Database("logs").Collection("logs")
|
||||
|
||||
opts := options.Find()
|
||||
opts.SetSort(bson.D{{"created_at", -1}})
|
||||
|
||||
cursor, err := collection.Find(context.TODO(), bson.D{}, opts)
|
||||
if err != nil {
|
||||
log.Println("Finding all docs error: ", err)
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var logs []*LogEntry
|
||||
|
||||
for cursor.Next(ctx) {
|
||||
var item LogEntry
|
||||
|
||||
err := cursor.Decode(&item)
|
||||
if err != nil {
|
||||
log.Println("Error decoding log into slice: ", err)
|
||||
return nil, err
|
||||
} else {
|
||||
logs = append(logs, &item)
|
||||
}
|
||||
}
|
||||
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
func (l *LogEntry) GetOne(id string) (*LogEntry, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
collection := client.Database("logs").Collection("logs")
|
||||
|
||||
docId, err := primitive.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var entry LogEntry
|
||||
err = collection.FindOne(ctx, bson.M{"_id": docId}).Decode(&entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &entry, nil
|
||||
}
|
||||
|
||||
func (l *LogEntry) DropCollections() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
collection := client.Database("logs").Collection("logs")
|
||||
|
||||
if err := collection.Drop(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LogEntry) Update() (*mongo.UpdateResult, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
collection := client.Database("logs").Collection("logs")
|
||||
docId, err := primitive.ObjectIDFromHex(l.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := collection.UpdateOne(
|
||||
ctx,
|
||||
bson.M{"_id": docId},
|
||||
bson.D{
|
||||
{"$set", bson.D{
|
||||
{"name", l.Name},
|
||||
{"data", l.Data},
|
||||
{"updated_at", time.Now()},
|
||||
}},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, err
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"logger/data"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/rpc"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const (
|
||||
mongoURL = "mongodb://mongo:27017"
|
||||
)
|
||||
|
||||
var client *mongo.Client
|
||||
|
||||
type Config struct {
|
||||
Models data.Models
|
||||
}
|
||||
|
||||
func main() {
|
||||
// connect to mongo
|
||||
mongoClient, err := connectToMongo()
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
client = mongoClient
|
||||
|
||||
// create a context to disconnect
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// close connection
|
||||
defer func() {
|
||||
if err = client.Disconnect(ctx); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
app := Config{
|
||||
Models: data.New(client),
|
||||
}
|
||||
}
|
||||
|
||||
func connectToMongo() (*mongo.Client, error) {
|
||||
// create connection options
|
||||
clientOptions := options.Client().ApplyURI(mongoURL)
|
||||
clientOptions.SetAuth(options.Credential{
|
||||
Username: "admin",
|
||||
Password: "password",
|
||||
})
|
||||
|
||||
c, err := mongo.Connect(context.TODO(), clientOptions)
|
||||
if err != nil {
|
||||
log.Println("Error connecting: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,345 @@
|
||||
# HTTP
|
||||
|
||||
The below snippets are using the `chi` package for Go.
|
||||
|
||||
### To Install
|
||||
|
||||
```bash
|
||||
go get github.com/go-chi/chi/v5
|
||||
go get github.com/go-chi/chi/v5/middleware
|
||||
go get github.com/go-chi/cors
|
||||
```
|
||||
|
||||
```go
|
||||
// main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
_ "github.com/jackc/pgconn"
|
||||
_ "github.com/jackc/pgx/v4"
|
||||
_ "github.com/jackc/pgx/v4/stdlib"
|
||||
)
|
||||
|
||||
const webPort = "80"
|
||||
|
||||
var counts int64
|
||||
|
||||
type Config struct {
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := Config{
|
||||
Client: &http.Client{},
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: fmt.Sprintf(":%s", webPort),
|
||||
Handler: app.routes(),
|
||||
}
|
||||
|
||||
err := srv.ListenAndServe()
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// routes.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/go-chi/cors"
|
||||
)
|
||||
|
||||
func (app *Config) routes() http.Handler {
|
||||
mux := chi.NewRouter()
|
||||
|
||||
mux.Use(cors.Handler(cors.Options{
|
||||
AllowedOrigins: []string{"https://*", "http://*"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
|
||||
ExposedHeaders: []string{"Link"},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 300,
|
||||
}))
|
||||
|
||||
mux.Use(middleware.Heartbeat("/ping"))
|
||||
|
||||
mux.Post("/authenticate", app.Authenticate)
|
||||
|
||||
return mux
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// handlers.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (app *Config) Authenticate(w http.ResponseWriter, r *http.Request) {
|
||||
var requestPayload struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
err := app.readJSON(w, r, &requestPayload)
|
||||
if err != nil {
|
||||
app.errorJSON(w, err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// validate user against db
|
||||
user, err := app.Repo.GetByEmail(requestPayload.Email)
|
||||
if err != nil {
|
||||
app.errorJSON(w, errors.New("invalid credentials"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
valid, err := app.Repo.PasswordMatches(requestPayload.Password, *user)
|
||||
if err != nil || !valid {
|
||||
app.errorJSON(w, errors.New("invalid credentials"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// log authentication
|
||||
err = app.logRequest("authentication", fmt.Sprintf("%s logged in", user.Email))
|
||||
if err != nil {
|
||||
app.errorJSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("Logged authentication request")
|
||||
|
||||
payload := jsonResponse{
|
||||
Error: false,
|
||||
Message: fmt.Sprintf("Logged in user %s", requestPayload.Email),
|
||||
Data: user,
|
||||
}
|
||||
|
||||
app.writeJSON(w, http.StatusAccepted, payload)
|
||||
}
|
||||
|
||||
func (app *Config) logRequest(name, data string) error {
|
||||
var entry struct {
|
||||
Name string `json:"name"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
entry.Name = name
|
||||
entry.Data = data
|
||||
|
||||
jsonData, _ := json.MarshalIndent(entry, "", "\t")
|
||||
|
||||
logURL := "http://logger/log"
|
||||
|
||||
request, err := http.NewRequest("POST", logURL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = app.Client.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```go
|
||||
// helpers.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type jsonResponse struct {
|
||||
Error bool `json:"error"`
|
||||
Message string `json:"message"`
|
||||
Data any `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func (app *Config) readJSON(w http.ResponseWriter, r *http.Request, data any) error {
|
||||
maxBytes := 1048576 // one MB
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes))
|
||||
|
||||
dec := json.NewDecoder(r.Body)
|
||||
err := dec.Decode(data)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = dec.Decode(&struct{}{})
|
||||
if err != io.EOF {
|
||||
return errors.New("Body must have only a single json value")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *Config) writeJSON(w http.ResponseWriter, status int, data any, headers ...http.Header) error {
|
||||
out, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(headers) > 0 {
|
||||
for key, val := range headers[0] {
|
||||
w.Header()[key] = val
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_, err = w.Write(out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *Config) errorJSON(w http.ResponseWriter, err error, status ...int) error {
|
||||
statusCode := http.StatusBadRequest
|
||||
|
||||
if len(status) > 0 {
|
||||
statusCode = status[0]
|
||||
}
|
||||
|
||||
var payload jsonResponse
|
||||
payload.Error = true
|
||||
payload.Message = err.Error()
|
||||
|
||||
return app.writeJSON(w, statusCode, payload)
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```go
|
||||
// handlers_test.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type RoundTripFunc func(req *http.Request) *http.Response
|
||||
|
||||
func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return f(req), nil
|
||||
}
|
||||
|
||||
func NewTestClient(fn RoundTripFunc) *http.Client {
|
||||
return &http.Client{
|
||||
Transport: fn,
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Authenticate(t *testing.T) {
|
||||
jsonToReturn := `
|
||||
{
|
||||
"error": false,
|
||||
"message": "some message"
|
||||
}
|
||||
`
|
||||
client := NewTestClient(func(req *http.Request) *http.Response {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewBufferString(jsonToReturn)),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
})
|
||||
testApp.Client = client
|
||||
|
||||
postBody := map[string]interface{}{
|
||||
"email": "me@here.com",
|
||||
"password": "verysecret",
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(postBody)
|
||||
|
||||
req, _ := http.NewRequest("POST", "/authenticate", bytes.NewReader(body))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler := http.HandlerFunc(testApp.Authenticate)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusAccepted {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v",
|
||||
rr.Code, http.StatusAccepted)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// routes_test.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
func Test_routes_exist(t *testing.T) {
|
||||
testApp := Config{}
|
||||
|
||||
testRoutes := testApp.routes()
|
||||
chiRoutes := testRoutes.(chi.Router)
|
||||
|
||||
routes := []string{"/authenticate"}
|
||||
|
||||
for _, route := range routes {
|
||||
routeExists(t, chiRoutes, route)
|
||||
}
|
||||
}
|
||||
|
||||
func routeExists(t *testing.T, routes chi.Router, route string) {
|
||||
found := false
|
||||
_ = chi.Walk(routes, func(method string, foundRoute string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
|
||||
if route == foundRoute {
|
||||
found = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if !found {
|
||||
t.Errorf("route %s not found", route)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
{ "text": "Testing", "link": "/go/advanced/testing" },
|
||||
{"text": "HTTP", "link": "/go/advanced/http"},
|
||||
{"text": "RPC", "link": "/go/advanced/rpc"},
|
||||
{"text": "gRPC", "link": "/go/advanced/grpc"}
|
||||
{"text": "gRPC", "link": "/go/advanced/grpc"},
|
||||
{"text": "Databases", "link": "/go/advanced/db"}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user