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

Setup integration test suite for db package #387

Merged
merged 6 commits into from
Dec 16, 2020
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions db/db_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package db_test

import (
"context"
"database/sql"
"fmt"
"testing"
"time"

"github.com/packethost/pkg/log"
"github.com/pkg/errors"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"github.com/tinkerbell/tink/db"
)

type NewPostgresDatabaseRequest struct {
ApplyMigration bool
}

// NewPostgresDatabaseClient returns a SQL client ready to be used. Behind the
// scene it is starting a Docker container that will get cleaned up when the
// test is over. Tests using this function are safe to run in parallel
func NewPostgresDatabaseClient(t *testing.T, ctx context.Context, req NewPostgresDatabaseRequest) (*sql.DB, *db.TinkDB, func() error) {
testcontainers.SkipIfProviderIsNotHealthy(t)
postgresC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
Image: "postgres:13.1",
ExposedPorts: []string{"5432/tcp"},
WaitingFor: wait.ForLog("database system is ready to accept connections"),
Env: map[string]string{
"POSTGRES_PASSWORD": "tinkerbell",
"POSTGRES_USER": "tinkerbell",
"POSTGRES_DB": "tinkerbell",
},
},
Started: true,
})
if err != nil {
t.Error(err)
}
port, err := postgresC.MappedPort(ctx, "5432")
if err != nil {
t.Error(err)
}
dbCon, err := sql.Open(
"postgres",
fmt.Sprintf(
"host=localhost port=%d user=%s password=%s dbname=%s sslmode=disable",
port.Int(),
"tinkerbell",
"tinkerbell",
"tinkerbell"))
if err != nil {
t.Error(err)
}

for ii := 0; ii < 5; ii++ {
err = dbCon.Ping()
if err != nil {
t.Log(errors.Wrap(err, "db check"))
time.Sleep(1 * time.Second)
continue
}
break
mmlb marked this conversation as resolved.
Show resolved Hide resolved
}
if err != nil {
t.Fatal(err)
}

tinkDB := db.Connect(dbCon, log.Test(t, "db-test"))
if req.ApplyMigration {
n, err := tinkDB.Migrate()
if err != nil {
t.Error(err)
}
t.Log(fmt.Sprintf("applied %d migrations", n))
}
return dbCon, tinkDB, func() error {
return postgresC.Terminate(ctx)
}
}
6 changes: 3 additions & 3 deletions db/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ func (d TinkDB) GetTemplate(ctx context.Context, fields map[string]string, delet
return "", "", "", err
}

// DeleteTemplate deletes a workflow template
func (d TinkDB) DeleteTemplate(ctx context.Context, name string) error {
// DeleteTemplate deletes a workflow template by id
func (d TinkDB) DeleteTemplate(ctx context.Context, id string) error {
tx, err := d.instance.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
if err != nil {
return errors.Wrap(err, "BEGIN transaction")
Expand All @@ -97,7 +97,7 @@ func (d TinkDB) DeleteTemplate(ctx context.Context, name string) error {
deleted_at = NOW()
WHERE
id = $1;
`, name)
`, id)
if err != nil {
return errors.Wrap(err, "UPDATE")
}
Expand Down
223 changes: 223 additions & 0 deletions db/template_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
package db_test

import (
"context"
"fmt"
"math/rand"
"strings"
"sync"
"testing"

"github.com/golang/protobuf/ptypes/timestamp"
"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
_ "github.com/lib/pq"
"github.com/tinkerbell/tink/db"
"github.com/tinkerbell/tink/workflow"
"gopkg.in/yaml.v2"
)

func TestCreateTemplate(t *testing.T) {
ctx := context.Background()

table := []struct {
// Name identifies the single test in a table test scenario
Name string
// Input is a list of workflows that will be used to pre-populate the database
Input []*workflow.Workflow
// InputAsync if set to true inserts all the input concurrently
InputAsync bool
// Expectation is the function used to apply the assertions.
// You can use it to validate if the Input are created as you expect
Expectation func(*testing.T, []*workflow.Workflow, *db.TinkDB)
// ExpectedErr is used to check for error during
// CreateTemplate execution. If you expect a particular error
// and you want to assert it, you can use this function
ExpectedErr func(*testing.T, error)
}{
{
Name: "happy-path-single-crete-template",
Input: []*workflow.Workflow{
func() *workflow.Workflow {
w := workflow.MustParseFromFile("./testdata/template_happy_path_1.yaml")
w.ID = "545f7ce9-5313-49c6-8704-0ed98814f1f7"
return w
}(),
},
Expectation: func(t *testing.T, input []*workflow.Workflow, tinkDB *db.TinkDB) {
wID, wName, wData, err := tinkDB.GetTemplate(context.Background(), map[string]string{"id": input[0].ID}, false)
if err != nil {
t.Error(err)
}
w := workflow.MustParse([]byte(wData))
w.ID = wID
w.Name = wName
if dif := cmp.Diff(input[0], w); dif != "" {
t.Errorf(dif)
}
},
},
{
Name: "create-two-template-same-name",
Input: []*workflow.Workflow{
func() *workflow.Workflow {
w := workflow.MustParseFromFile("./testdata/template_happy_path_1.yaml")
w.ID = "545f7ce9-5313-49c6-8704-0ed98814f1f7"
return w
}(),
func() *workflow.Workflow {
w := workflow.MustParseFromFile("./testdata/template_happy_path_1.yaml")
w.ID = "aaaaaaaa-5313-49c6-8704-bbbbbbbbbbbb"
return w
}(),
},
ExpectedErr: func(t *testing.T, err error) {
if err == nil {
t.Error("expected error, got nil")
}
if !strings.Contains(err.Error(), "pq: duplicate key value violates unique constraint \"uidx_template_name\"") {
t.Errorf("\nexpected err: %s\ngot: %s", "pq: duplicate key value violates unique constraint \"uidx_template_name\"", err)
}
},
},
{
Name: "update-on-create",
Input: []*workflow.Workflow{
func() *workflow.Workflow {
w := workflow.MustParseFromFile("./testdata/template_happy_path_1.yaml")
w.ID = "545f7ce9-5313-49c6-8704-0ed98814f1f7"
return w
}(),
func() *workflow.Workflow {
w := workflow.MustParseFromFile("./testdata/template_happy_path_1.yaml")
w.ID = "545f7ce9-5313-49c6-8704-0ed98814f1f7"
w.Name = "updated-name"
return w
}(),
},
Expectation: func(t *testing.T, input []*workflow.Workflow, tinkDB *db.TinkDB) {
_, wName, _, err := tinkDB.GetTemplate(context.Background(), map[string]string{"id": input[0].ID}, false)
if err != nil {
t.Error(err)
}
if wName != "updated-name" {
t.Errorf("expected name to be \"%s\", got \"%s\"", "updated-name", wName)
}
},
},
{
Name: "create-stress-test",
InputAsync: true,
Input: func() []*workflow.Workflow {
input := []*workflow.Workflow{}
for ii := 0; ii < 20; ii++ {
w := workflow.MustParseFromFile("./testdata/template_happy_path_1.yaml")
w.ID = uuid.New().String()
w.Name = fmt.Sprintf("id_%d", rand.Int())
t.Log(w.Name)
input = append(input, w)
}
return input
}(),
ExpectedErr: func(t *testing.T, err error) {
if err != nil {
t.Error(err)
}
},
Expectation: func(t *testing.T, input []*workflow.Workflow, tinkDB *db.TinkDB) {
count := 0
err := tinkDB.ListTemplates("%", func(id, n string, in, del *timestamp.Timestamp) error {
count = count + 1
return nil
})
if err != nil {
t.Error(err)
}
if len(input) != count {
t.Errorf("expected %d templates stored in the database but we got %d", len(input), count)
}
},
},
}

for _, s := range table {
t.Run(s.Name, func(t *testing.T) {
t.Parallel()
_, tinkDB, cl := NewPostgresDatabaseClient(t, ctx, NewPostgresDatabaseRequest{
ApplyMigration: true,
})
defer func() {
err := cl()
if err != nil {
t.Error(err)
}
}()
var wg sync.WaitGroup
wg.Add(len(s.Input))
for _, tt := range s.Input {
if s.InputAsync {
go func(ctx context.Context, tinkDB *db.TinkDB, tt *workflow.Workflow) {
defer wg.Done()
err := createTemplateFromWorkflowType(ctx, tinkDB, tt)
if err != nil {
s.ExpectedErr(t, err)
}
}(ctx, tinkDB, tt)
} else {
wg.Done()
err := createTemplateFromWorkflowType(ctx, tinkDB, tt)
if err != nil {
s.ExpectedErr(t, err)
}
}
}
wg.Wait()
s.Expectation(t, s.Input, tinkDB)
})
}
}

func TestCreateTemplate_TwoTemplateWithSameNameButFirstOneIsDeleted(t *testing.T) {
t.Parallel()
ctx := context.Background()
_, tinkDB, cl := NewPostgresDatabaseClient(t, ctx, NewPostgresDatabaseRequest{
ApplyMigration: true,
})
defer func() {
err := cl()
if err != nil {
t.Error(err)
}
}()

w := workflow.MustParseFromFile("./testdata/template_happy_path_1.yaml")
w.ID = "545f7ce9-5313-49c6-8704-0ed98814f1f7"
err := createTemplateFromWorkflowType(ctx, tinkDB, w)
if err != nil {
t.Error(err)
}
err = tinkDB.DeleteTemplate(ctx, w.ID)
if err != nil {
t.Error(err)
}

ww := workflow.MustParseFromFile("./testdata/template_happy_path_1.yaml")
ww.ID = "1111aaaa-5313-49c6-8704-222222aaaaaa"
err = createTemplateFromWorkflowType(ctx, tinkDB, ww)
if err != nil {
t.Error(err)
}
}

func createTemplateFromWorkflowType(ctx context.Context, tinkDB *db.TinkDB, tt *workflow.Workflow) error {
uID := uuid.MustParse(tt.ID)
content, err := yaml.Marshal(tt)
if err != nil {
return err
}
err = tinkDB.CreateTemplate(ctx, tt.Name, string(content), uID)
if err != nil {
return err
}
return nil
}
19 changes: 19 additions & 0 deletions db/testdata/template_happy_path_1.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: '0.1'
name: packet_osie_provision
global_timeout: 600
tasks:
- name: "run_one_worker"
worker: "{{.device_1}}"
environment:
MIRROR_HOST: 192.168.1.2
actions:
- name: "server_partitioning"
image: update-data
timeout: 60
environment:
NGINX_HOST: 192.168.1.2
- name: "update_db"
image: update-data
timeout: 50
environment:
MIRROR_HOST: 192.168.1.3
12 changes: 4 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,22 @@ module github.com/tinkerbell/tink
go 1.13

require (
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
github.com/Microsoft/go-winio v0.4.14 // indirect
github.com/containerd/containerd v1.3.2 // indirect
github.com/docker/distribution v2.7.1+incompatible
github.com/docker/docker v1.4.2-0.20191212201129-5f9f41018e9d
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/docker v17.12.0-ce-rc1.0.20200916142827-bd33bbf0497b+incompatible
github.com/docker/go-units v0.4.0 // indirect
github.com/go-openapi/strfmt v0.19.3 // indirect
github.com/golang/protobuf v1.4.2
github.com/google/go-cmp v0.5.2 // indirect
github.com/google/go-cmp v0.5.2
github.com/google/uuid v1.1.2
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
github.com/grpc-ecosystem/grpc-gateway v1.15.2
github.com/jedib0t/go-pretty v4.3.0+incompatible
github.com/lib/pq v1.2.1-0.20191011153232-f91d3411e481
github.com/mattn/go-runewidth v0.0.5 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/packethost/pkg v0.0.0-20200903155310-0433e0605550
github.com/pkg/errors v0.8.1
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.3.0
github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351
github.com/sirupsen/logrus v1.4.2
Expand All @@ -32,6 +27,7 @@ require (
github.com/spf13/viper v1.7.0
github.com/stormcat24/protodep v0.0.0-20200505140716-b02c9ba62816
github.com/stretchr/testify v1.6.1
github.com/testcontainers/testcontainers-go v0.9.0
go.mongodb.org/mongo-driver v1.1.2 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.16.0 // indirect
Expand Down
Loading