mirror of
https://gitlab.com/djdietrick/docs
synced 2026-05-03 01:30:55 -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": "Testing", "link": "/go/advanced/testing" },
|
||||||
{"text": "HTTP", "link": "/go/advanced/http"},
|
{"text": "HTTP", "link": "/go/advanced/http"},
|
||||||
{"text": "RPC", "link": "/go/advanced/rpc"},
|
{"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