Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add WebhookRepository #2

Merged
merged 1 commit into from Mar 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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),
}
}