Skip to content

Commit

Permalink
feat: Add WebhookRepository (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
allisson committed Mar 2, 2021
1 parent 846ef5c commit f1bea1b
Show file tree
Hide file tree
Showing 13 changed files with 398 additions and 30 deletions.
18 changes: 17 additions & 1 deletion .github/workflows/verify.yml
Expand Up @@ -10,6 +10,20 @@ jobs:
verify:
name: Test
runs-on: ubuntu-latest
services:
postgres:
image: postgres:12-alpine
env:
POSTGRES_DB: postmand
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Set up Go 1.16
uses: actions/setup-go@v2
Expand Down Expand Up @@ -38,4 +52,6 @@ jobs:
args: -E gosec

- name: Run Tests
run: go test -covermode=count -coverprofile=count.out -v ./...
env:
POSTMAND_DATABASE_URL: "postgres://test:test@localhost:5432/postmand?sslmode=disable"
run: make db-migrate && go test -covermode=count -coverprofile=count.out -v ./...
6 changes: 6 additions & 0 deletions .gitignore
Expand Up @@ -15,4 +15,10 @@
# vendor/

*.log

# golangci-lint
bin/

# golang migrate
migrate.darwin-amd64
migrate.linux-amd64
11 changes: 10 additions & 1 deletion Makefile
Expand Up @@ -10,4 +10,13 @@ lint:
test:
go test -covermode=count -coverprofile=count.out -v ./...

.PHONY: lint test
download-golang-migrate-binary:
if [ ! -f ./migrate.$(PLATFORM)-amd64 ] ; \
then \
curl -sfL https://github.com/golang-migrate/migrate/releases/download/v4.14.1/migrate.$(PLATFORM)-amd64.tar.gz | tar -xvz; \
fi;

db-migrate: download-golang-migrate-binary
./migrate.$(PLATFORM)-amd64 -source file://db/migrations -database ${POSTMAND_DATABASE_URL} up

.PHONY: lint test download-golang-migrate-binary db-migrate
1 change: 1 addition & 0 deletions db/migrations/000001_create_initial_schema.down.sql
@@ -0,0 +1 @@
DROP TABLE IF EXISTS webhooks;
21 changes: 21 additions & 0 deletions db/migrations/000001_create_initial_schema.up.sql
@@ -0,0 +1,21 @@
-- webhooks table

CREATE TABLE IF NOT EXISTS webhooks(
id uuid PRIMARY KEY,
name VARCHAR NOT NULL,
url VARCHAR NOT NULL,
content_type VARCHAR NOT NULL,
valid_status_codes SMALLINT[] NOT NULL,
secret_token VARCHAR NOT NULL,
active BOOLEAN NOT NULL,
max_delivery_attempts SMALLINT NOT NULL,
delivery_attempt_timeout SMALLINT NOT NULL,
retry_min_backoff SMALLINT NOT NULL,
retry_max_backoff SMALLINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS webhooks_name_idx ON webhooks (name);
CREATE INDEX IF NOT EXISTS webhooks_active_idx ON webhooks (active);
CREATE INDEX IF NOT EXISTS webhooks_created_at_idx ON webhooks USING BRIN(created_at);
51 changes: 30 additions & 21 deletions entity.go
Expand Up @@ -6,30 +6,38 @@ import (
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/go-ozzo/ozzo-validation/v4/is"
"github.com/google/uuid"
"github.com/lib/pq"
)

const (
// DeliveryStatusPending represents the delivery pending status
DeliveryStatusPending = "pending"
// DeliveryStatusTodo represents the delivery todo status
DeliveryStatusTodo = "todo"
// DeliveryStatusDoing represents the delivery doing status
DeliveryStatusDoing = "doing"
// DeliveryStatusSucceeded represents the delivery succeeded status
DeliveryStatusSucceeded = "succeeded"
// DeliveryStatusFailed represents the delivery failed status
DeliveryStatusFailed = "failed"
// DeliveryStatusCompleted represents the delivery completed status
DeliveryStatusCompleted = "completed"
)

// ID represents the primary key for all entities.
type ID = uuid.UUID

// Webhook represents a webhook in the system.
type Webhook struct {
ID uuid.UUID `json:"id" db:"id"`
Name string `json:"name" db:"name"`
URL string `json:"url" db:"url"`
ContentType string `json:"content_type" db:"content_type"`
SecretToken string `json:"secret_token" db:"secret_token"`
MaxDeliveryAttempts int `json:"max_delivery_attempts" db:"max_delivery_attempts"`
DeliveryAttemptTimeout int `json:"delivery_attempt_timeout" db:"delivery_attempt_timeout"`
RetryMinBackoff int `json:"retry_min_backoff" db:"retry_min_backoff"`
RetryMaxBackoff int `json:"retry_max_backoff" db:"retry_max_backoff"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
ID ID `json:"id" db:"id"`
Name string `json:"name" db:"name"`
URL string `json:"url" db:"url"`
ContentType string `json:"content_type" db:"content_type"`
ValidStatusCodes pq.Int32Array `json:"valid_status_codes" db:"valid_status_codes"`
SecretToken string `json:"secret_token" db:"secret_token"`
Active bool `json:"active" db:"active"`
MaxDeliveryAttempts int `json:"max_delivery_attempts" db:"max_delivery_attempts"`
DeliveryAttemptTimeout int `json:"delivery_attempt_timeout" db:"delivery_attempt_timeout"`
RetryMinBackoff int `json:"retry_min_backoff" db:"retry_min_backoff"`
RetryMaxBackoff int `json:"retry_max_backoff" db:"retry_max_backoff"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}

// Validate implements ozzo validation Validatable interface
Expand All @@ -39,6 +47,7 @@ func (w Webhook) Validate() error {
validation.Field(&w.Name, validation.Required, validation.Length(3, 255)),
validation.Field(&w.URL, validation.Required, is.URL),
validation.Field(&w.ContentType, validation.Required, validation.In("application/x-www-form-urlencoded", "application/json")),
validation.Field(&w.ValidStatusCodes, validation.Required),
validation.Field(&w.MaxDeliveryAttempts, validation.Required, validation.Min(1)),
validation.Field(&w.DeliveryAttemptTimeout, validation.Required, validation.Min(1)),
validation.Field(&w.RetryMinBackoff, validation.Required, validation.Min(1)),
Expand All @@ -48,8 +57,8 @@ func (w Webhook) Validate() error {

// Delivery represents a payload that must be delivery using webhook context.
type Delivery struct {
ID uuid.UUID `json:"id" db:"id"`
WebhookID uuid.UUID `json:"webhook_id" db:"webhook_id"`
ID ID `json:"id" db:"id"`
WebhookID ID `json:"webhook_id" db:"webhook_id"`
Payload string `json:"payload" db:"payload"`
ScheduledAt time.Time `json:"scheduled_at" db:"scheduled_at"`
DeliveryAttempts int `json:"delivery_attempts" db:"delivery_attempts"`
Expand All @@ -64,15 +73,15 @@ func (d Delivery) Validate() error {
validation.Field(&d.ID, validation.Required, is.UUIDv4),
validation.Field(&d.WebhookID, validation.Required, is.UUIDv4),
validation.Field(&d.ScheduledAt, validation.Required),
validation.Field(&d.Status, validation.Required, validation.In("pending", "completed", "failed")),
validation.Field(&d.Status, validation.Required, validation.In("todo", "doing", "succeeded", "failed")),
)
}

// DeliveryAttempt represents a delivery attempt.
type DeliveryAttempt struct {
ID uuid.UUID `json:"id" db:"id"`
WebhookID uuid.UUID `json:"destination_id" db:"destination_id"`
DeliveryID uuid.UUID `json:"delivery_id" db:"delivery_id"`
ID ID `json:"id" db:"id"`
WebhookID ID `json:"destination_id" db:"destination_id"`
DeliveryID ID `json:"delivery_id" db:"delivery_id"`
ResponseHeaders string `json:"response_headers" db:"response_headers"`
ResponseBody string `json:"response_body" db:"response_body"`
ResponseStatusCode int `json:"response_status_code" db:"response_status_code"`
Expand Down
24 changes: 18 additions & 6 deletions entity_test.go
Expand Up @@ -7,6 +7,8 @@ import (
"time"

"github.com/google/uuid"
"github.com/lib/pq"
_ "github.com/lib/pq"
"github.com/stretchr/testify/assert"
)

Expand All @@ -19,21 +21,21 @@ func TestWebhook(t *testing.T) {
{
"required fields",
Webhook{},
`{"content_type":"cannot be blank","delivery_attempt_timeout":"cannot be blank","id":"must be a valid UUID v4","max_delivery_attempts":"cannot be blank","name":"cannot be blank","retry_max_backoff":"cannot be blank","retry_min_backoff":"cannot be blank","url":"cannot be blank"}`,
`{"content_type":"cannot be blank","delivery_attempt_timeout":"cannot be blank","id":"must be a valid UUID v4","max_delivery_attempts":"cannot be blank","name":"cannot be blank","retry_max_backoff":"cannot be blank","retry_min_backoff":"cannot be blank","url":"cannot be blank","valid_status_codes":"cannot be blank"}`,
},
{
"Short name",
Webhook{ID: uuid.New(), Name: "A", URL: "https://httpbin.org/post", ContentType: "application/json", MaxDeliveryAttempts: 1, DeliveryAttemptTimeout: 1, RetryMinBackoff: 1, RetryMaxBackoff: 1},
Webhook{ID: uuid.New(), Name: "A", URL: "https://httpbin.org/post", ContentType: "application/json", ValidStatusCodes: pq.Int32Array{200, 201}, MaxDeliveryAttempts: 1, DeliveryAttemptTimeout: 1, RetryMinBackoff: 1, RetryMaxBackoff: 1},
`{"name":"the length must be between 3 and 255"}`,
},
{
"Long name",
Webhook{ID: uuid.New(), Name: strings.Repeat("A", 300), URL: "https://httpbin.org/post", ContentType: "application/json", MaxDeliveryAttempts: 1, DeliveryAttemptTimeout: 1, RetryMinBackoff: 1, RetryMaxBackoff: 1},
Webhook{ID: uuid.New(), Name: strings.Repeat("A", 300), URL: "https://httpbin.org/post", ContentType: "application/json", ValidStatusCodes: pq.Int32Array{200, 201}, MaxDeliveryAttempts: 1, DeliveryAttemptTimeout: 1, RetryMinBackoff: 1, RetryMaxBackoff: 1},
`{"name":"the length must be between 3 and 255"}`,
},
{
"Content type invalid option",
Webhook{ID: uuid.New(), Name: "AAA", URL: "https://httpbin.org/post", ContentType: "text/html", MaxDeliveryAttempts: 1, DeliveryAttemptTimeout: 1, RetryMinBackoff: 1, RetryMaxBackoff: 1},
Webhook{ID: uuid.New(), Name: "AAA", URL: "https://httpbin.org/post", ContentType: "text/html", ValidStatusCodes: pq.Int32Array{200, 201}, MaxDeliveryAttempts: 1, DeliveryAttemptTimeout: 1, RetryMinBackoff: 1, RetryMaxBackoff: 1},
`{"content_type":"must be a valid value"}`,
},
}
Expand All @@ -47,7 +49,17 @@ func TestWebhook(t *testing.T) {
})
}

webhook := Webhook{ID: uuid.New(), Name: "AAA", URL: "https://httpbin.org/post", ContentType: "application/json", MaxDeliveryAttempts: 1, DeliveryAttemptTimeout: 1, RetryMinBackoff: 1, RetryMaxBackoff: 1}
webhook := Webhook{
ID: uuid.New(),
Name: "AAA",
URL: "https://httpbin.org/post",
ContentType: "application/json",
ValidStatusCodes: pq.Int32Array{200, 201},
MaxDeliveryAttempts: 1,
DeliveryAttemptTimeout: 1,
RetryMinBackoff: 1,
RetryMaxBackoff: 1,
}
err := webhook.Validate()
assert.Nil(t, err)
}
Expand Down Expand Up @@ -84,7 +96,7 @@ func TestDelivery(t *testing.T) {
WebhookID: uuid.New(),
Payload: `{"success": true}`,
ScheduledAt: time.Now().UTC(),
Status: "pending",
Status: "todo",
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
Expand Down
4 changes: 4 additions & 0 deletions go.mod
Expand Up @@ -3,7 +3,11 @@ module github.com/allisson/postmand
go 1.16

require (
github.com/DATA-DOG/go-txdb v0.1.3
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
github.com/google/uuid v1.2.0
github.com/huandu/go-sqlbuilder v1.12.0
github.com/jmoiron/sqlx v1.3.1
github.com/lib/pq v1.9.0
github.com/stretchr/testify v1.7.0
)
20 changes: 19 additions & 1 deletion go.sum
@@ -1,11 +1,29 @@
github.com/DATA-DOG/go-txdb v0.1.3 h1:R4v6OuOcy2O147e2zHxU0B4NDtF+INb5R9q/CV7AEMg=
github.com/DATA-DOG/go-txdb v0.1.3/go.mod h1:DhAhxMXZpUJVGnT+p9IbzJoRKvlArO2pkHjnGX7o0n0=
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0=
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/huandu/go-assert v1.1.5 h1:fjemmA7sSfYHJD7CUqs9qTwwfdNAx7/j2/ZlHXzNB3c=
github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U=
github.com/huandu/go-sqlbuilder v1.12.0 h1:QSmKkoIKaZTZBNROweq/c3wTxqXhuuAhbTWPtbpVsNA=
github.com/huandu/go-sqlbuilder v1.12.0/go.mod h1:LILlbQo0MOYjlIiGgOSR3UcWQpd5Y/oZ7HLNGyAUz0E=
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/jmoiron/sqlx v1.3.1 h1:aLN7YINNZ7cYOPK3QC83dbM6KT0NMqVMw961TqrejlE=
github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8=
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand Down
23 changes: 23 additions & 0 deletions repository.go
@@ -0,0 +1,23 @@
package postmand

// RepositoryGetOptions contains options used in the Get methods.
type RepositoryGetOptions struct {
Filters map[string]interface{}
}

// RepositoryListOptions contains options used in the List methods.
type RepositoryListOptions struct {
Filters map[string]interface{}
Limit int
Offset int
OrderBy string
}

// WebhookRepository is the interface that will be used to iterate with the Webhook data.
type WebhookRepository interface {
Get(getOptions *RepositoryGetOptions) (*Webhook, error)
List(listOptions *RepositoryListOptions) ([]*Webhook, error)
Create(webhook *Webhook) error
Update(webhook *Webhook) error
Delete(id ID) error
}
31 changes: 31 additions & 0 deletions repository/util_test.go
@@ -0,0 +1,31 @@
package repository

import (
"fmt"
"math/rand"
"os"
"time"

"github.com/DATA-DOG/go-txdb"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
)

func init() {
txdb.Register("pgx", "postgres", os.Getenv("POSTMAND_DATABASE_URL"))
rand.Seed(time.Now().UnixNano())
}

type testHelper struct {
db *sqlx.DB
webhookRepository *Webhook
}

func newTestHelper() testHelper {
cName := fmt.Sprintf("connection_%d", time.Now().UnixNano())
db, _ := sqlx.Open("pgx", cName)
return testHelper{
db: db,
webhookRepository: NewWebhook(db),
}
}

0 comments on commit f1bea1b

Please sign in to comment.