Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Go

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18

- name: Build
run: go build -v ./...

- name: Test
run: go test -race -covermode=atomic -coverprofile coverage.txt ./...

- name: Build coverage
run: go tool cover -html=coverage.txt -o coverage.html

- name: Log coverage
run: bash <(curl -s https://codecov.io/bash)
14 changes: 0 additions & 14 deletions .travis.yml

This file was deleted.

10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,13 @@ Total | 44832 | 49714857
The _Entries_ column provides the number of entries in a cache bin,
while the _Size_ bin provides the size used by keys and data in Redis
storage, based on information provided by the `MEMORY USAGE` command.

### Testing

- Run only unit tests: `make test`
- Run unit and integration tests: `make test-ci`, assuming:
- on `localhost:6379`: an unauthenticated Redis instance
- on `localhost:6380`: a Redis instance with:
- `ACL SETUSER alice on ~* &* +@all nopass`
- `ACL SETUSER bob on ~* &* +@all >testpass`
- a Redis instance with `requirepass testpass` active on `localhost:6380`
103 changes: 103 additions & 0 deletions dock/dock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package dock

import (
"database/sql"
"errors"
"fmt"
"log"

_ "github.com/jackc/pgx/v4/stdlib"
)

type PgAdapter struct {
conn *sql.DB
pass string
}

func (pg *PgAdapter) CreatePhoneNumber(num string) (int, error) {
if pg.conn == nil {
return 0, errors.New("no connection")
}
const sq = `
INSERT INTO numbers(number)
VALUES($1)
RETURNING id;
`
// LastInsertId() is not supported by pgx, so we need this construct.
var id int
err := pg.conn.QueryRow(sq, num).Scan(&id)
if err != nil {
return 0, fmt.Errorf("failed inserting %s: %w", num, err)
}
return id, nil
}

func (pg *PgAdapter) RemovePhoneNumber(id int) error {
if pg.conn == nil {
return errors.New("no connection")
}
const sq = `
DELETE FROM numbers
WHERE id=$1;
`
res, err := pg.conn.Exec(sq, id)
if err != nil {
return fmt.Errorf("failed deleting id %d: %w", id, err)
}
ra, err := res.RowsAffected()
if err != nil {
return fmt.Errorf("failed getting deleted rows for id %d: %w", id, err)
}
if ra != 1 {
return fmt.Errorf("deleted %d rows for id %d, but expected 1", ra, id)
}
return nil
}

type Option func(adapter *PgAdapter)

func WithPassword(pass string) func(adapter *PgAdapter) {
return func(adapter *PgAdapter) {
adapter.pass = pass
}
}

func NewAdapter(host, port, user, base string, options ...Option) (*PgAdapter, error) {
pg := &PgAdapter{}
for _, option := range options {
option(pg)
}
c, err := sql.Open("pgx", fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable",
user, pg.pass, host, port, base))
if err != nil {
return nil, fmt.Errorf("opening DB: %w", err)
}
if err := c.Ping(); err != nil {
log.Printf("Failed ping: %v", err)
return nil, fmt.Errorf("pinging: %v", err)
}

pg.conn = c
return pg, nil
}

func initTestAdapter(pg *PgAdapter) {
if pg.conn == nil {
panic(errors.New("connection not open"))
}
sq := `
CREATE TABLE public.numbers (
id SERIAL NOT NULL,
number character varying NOT NULL
);
ALTER TABLE ONLY public.numbers
ADD CONSTRAINT numbers_pk PRIMARY KEY (id);
`
res, err := pg.conn.Exec(sq)
if err != nil {
panic(err)
}
li, _ := res.LastInsertId()
ra, _ := res.RowsAffected()
log.Printf("Created table: %v %v", li, ra)
}
106 changes: 106 additions & 0 deletions dock/docker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package dock

import (
"fmt"
"log"
"os"
"testing"

"github.com/ory/dockertest"
)

const testUser = "postgres"
const testPassword = "password"
const testHost = "localhost"
const testDbName = "phone_numbers"

var (
pool *dockertest.Pool
testPort string
)

// getAdapter retrieves the Postgres adapter with test credentials
func getAdapter() (*PgAdapter, error) {
return NewAdapter(testHost, testPort, testUser, testDbName, WithPassword(testPassword))
}

// setup instantiates a Postgres docker container and attempts to connect to it via a new adapter
func setup() *dockertest.Resource {
var err error
pool, err = dockertest.NewPool("")
if err != nil {
log.Fatalf("could not connect to docker: %s", err)
}

// Pulls an image, creates a container based on it and runs it
resource, err := pool.Run("postgres", "14", []string{fmt.Sprintf("POSTGRES_PASSWORD=%s", testPassword), fmt.Sprintf("POSTGRES_DB=%s", testDbName)})
if err != nil {
log.Fatalf("could not start resource: %s", err)
}
testPort = resource.GetPort("5432/tcp") // Set port used to communicate with Postgres

var adapter *PgAdapter
// Exponential backoff-retry, because the application in the container might not be ready to accept connections yet
if err := pool.Retry(func() error {
adapter, err = getAdapter()
return err
}); err != nil {
log.Fatalf("could not connect to docker: %s", err)
}

initTestAdapter(adapter)

return resource
}

func clear(res *dockertest.Resource) {
if res != nil {
if err := res.Close(); err != nil {
log.Printf("failed closing resource: %v", err)
}
}
}

func TestMain(m *testing.M) {
r := setup()
code := m.Run()
clear(r)
os.Exit(code)
}

func TestCreatePhoneNumber(t *testing.T) {
testNumber := "1234566656"
adapter, err := getAdapter()
if err != nil {
t.Fatalf("error creating new test adapter: %v", err)
}

cases := []struct {
error bool
description string
}{
{
description: "Should succeed with valid creation of a phone number",
},
{
description: "Should fail if database connection closed",
error: true,
},
}
for _, c := range cases {
t.Run(c.description, func(t *testing.T) {
if c.error {
adapter.conn.Close()
}
id, err := adapter.CreatePhoneNumber(testNumber)
if !c.error && err != nil {
t.Errorf("expecting no error but received: %v", err)
} else if !c.error { // Remove test number from db so not captured by following tests
err = adapter.RemovePhoneNumber(id)
if err != nil {
t.Fatalf("error removing test number from database")
}
}
})
}
}
31 changes: 30 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,37 @@ go 1.21

require (
github.com/gomodule/redigo v1.8.9
github.com/jackc/pgx/v4 v4.18.1
github.com/morikuni/aec v1.0.0
github.com/ory/dockertest v3.3.5+incompatible
golang.org/x/term v0.17.0
)

require golang.org/x/sys v0.17.0 // indirect
require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/containerd/continuity v0.3.0 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/gotestyourself/gotestyourself v2.2.0+incompatible // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.14.1 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
github.com/jackc/pgtype v1.14.2 // indirect
github.com/lib/pq v1.10.6 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/opencontainers/runc v1.1.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect
gotest.tools v2.2.0+incompatible // indirect
)
Loading