Skip to content

Commit

Permalink
feature: api tests setup (#8)
Browse files Browse the repository at this point in the history
* initial api test setup with mongo, voconed and faucet containers
* first endpoint test
* some debug logs removed and bugs fixed
  • Loading branch information
lucasmenendez authored Sep 6, 2024
1 parent e1708b8 commit 9b1bb20
Show file tree
Hide file tree
Showing 9 changed files with 390 additions and 18 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ jobs:
exit 1
fi
- name: Run Go test
run: go test ./...
run: go test -v -timeout=30m ./...
- name: Run Go test -race
if: github.ref == 'refs/heads/stage' || startsWith(github.ref, 'refs/heads/release')
run: go test -vet=off -timeout=15m -race ./... # note that -race can easily make the crypto stuff 10x slower
run: go test -vet=off -timeout=30m -race ./... # note that -race can easily make the crypto stuff 10x slower

docker-release:
runs-on: ubuntu-latest
Expand Down
13 changes: 2 additions & 11 deletions account/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"time"

"github.com/ethereum/go-ethereum/common"
"github.com/spf13/viper"
vocdoniapi "go.vocdoni.io/dvote/api"
"go.vocdoni.io/dvote/apiclient"
"go.vocdoni.io/dvote/crypto/ethereum"
Expand All @@ -29,32 +28,28 @@ func New(privateKey string, apiEndpoint string) (*Account, error) {
if err != nil {
return nil, fmt.Errorf("failed to create API client: %w", err)
}
if err := apiClient.SetAccount(viper.GetString("privateKey")); err != nil {
if err := apiClient.SetAccount(privateKey); err != nil {
return nil, fmt.Errorf("failed to set account: %w", err)
}
if err := ensureAccountExist(apiClient); err != nil {
return nil, fmt.Errorf("failed to ensure account exists: %w", err)
}

// create the signer
signer := ethereum.SignKeys{}
if err := signer.AddHexKey(privateKey); err != nil {
return nil, err
}

// get account and log some info
account, err := apiClient.Account("")
if err != nil {
log.Fatalf("failed to get account: %v", err)
}

log.Infow("Vocdoni account initialized",
"endpoint", apiEndpoint,
"chainID", apiClient.ChainID(),
"address", account.Address,
"balance", account.Balance,
)

return &Account{
client: apiClient,
signer: &signer,
Expand All @@ -68,13 +63,9 @@ func (a *Account) FaucetPackage(toAddr string, amount uint64) (*models.FaucetPac

// ensureAccountExist checks if the account exists and creates it if it doesn't.
func ensureAccountExist(cli *apiclient.HTTPclient) error {
account, err := cli.Account("")
if err == nil {
log.Infow("account already exists", "address", account.Address)
if _, err := cli.Account(""); err == nil {
return nil
}

log.Infow("creating new account", "address", cli.MyAddress().Hex())
faucetPkg, err := apiclient.GetFaucetPackageFromDefaultService(cli.MyAddress().Hex(), cli.ChainID())
if err != nil {
return fmt.Errorf("failed to get faucet package: %w", err)
Expand Down
150 changes: 150 additions & 0 deletions api/api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package api

import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"testing"
"time"

"github.com/vocdoni/saas-backend/account"
"github.com/vocdoni/saas-backend/db"
"github.com/vocdoni/saas-backend/test"
"go.vocdoni.io/dvote/apiclient"
)

type apiTestCase struct {
uri string
method string
body []byte
expectedStatus int
expectedBody []byte
}

const (
testSecret = "super-secret"
testEmail = "[email protected]"
testPass = "password123"
testHost = "0.0.0.0"
testPort = 7788
)

// testDB is the MongoDB storage for the tests. Make it global so it can be
// accessed by the tests directly.
var testDB *db.MongoStorage

// testURL helper function returns the full URL for the given path using the
// test host and port.
func testURL(path string) string {
return fmt.Sprintf("http://%s:%d%s", testHost, testPort, path)
}

// mustMarshal helper function marshalls the input interface into a byte slice.
// It panics if the marshalling fails.
func mustMarshal(i any) []byte {
b, err := json.Marshal(i)
if err != nil {
panic(err)
}
return b
}

// pingAPI helper function pings the API endpoint and retries the request
// if it fails until the retries limit is reached. It returns an error if the
// request fails or the status code is not 200 as many times as the retries
// limit.
func pingAPI(endpoint string, retries int) error {
// create a new ping request
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil {
return err
}
// try to ping the API
var pingErr error
for i := 0; i < retries; i++ {
var resp *http.Response
if resp, pingErr = http.DefaultClient.Do(req); pingErr == nil {
if resp.StatusCode == http.StatusOK {
return nil
}
pingErr = fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
time.Sleep(time.Second)
}
return pingErr
}

// TestMain function starts the MongoDB container, the Voconed container, and
// the API server before running the tests. It also creates a new MongoDB
// connection with a random database name, a new Voconed API client, and a new
// account with the Voconed private key and the API container endpoint. It
// starts the API server and waits for it to start before running the tests.
func TestMain(m *testing.M) {
ctx := context.Background()
// start a MongoDB container for testing
dbContainer, err := test.StartMongoContainer(ctx)
if err != nil {
panic(err)
}
// ensure the container is stopped when the test finishes
defer func() { _ = dbContainer.Terminate(ctx) }()
// get the MongoDB connection string
mongoURI, err := dbContainer.Endpoint(ctx, "mongodb")
if err != nil {
panic(err)
}
// start the faucet container
faucetContainer, err := test.StartVocfaucetContainer(ctx)
if err != nil {
panic(err)
}
defer func() { _ = faucetContainer.Terminate(ctx) }()
// start the voconed container
apiContainer, err := test.StartVoconedContainer(ctx)
if err != nil {
panic(err)
}
defer func() { _ = apiContainer.Terminate(ctx) }()
// get the API endpoint
apiEndpoint, err := apiContainer.Endpoint(ctx, "http")
if err != nil {
panic(err)
}
testAPIEndpoint := test.VoconedAPIURL(apiEndpoint)
// set reset db env var to true
_ = os.Setenv("VOCDONI_MONGO_RESET_DB", "true")
// create a new MongoDB connection with the test database
if testDB, err = db.New(mongoURI, test.RandomDatabaseName()); err != nil {
panic(err)
}
defer testDB.Close()
// create the remote test API client
testAPIClient, err := apiclient.New(testAPIEndpoint)
if err != nil {
panic(err)
}
// create the test account with the Voconed private key and the API
// container endpoint
testAccount, err := account.New(test.VoconedFoundedPrivKey, testAPIEndpoint)
if err != nil {
panic(err)
}
// start the API
New(&APIConfig{
Host: testHost,
Port: testPort,
Secret: testSecret,
DB: testDB,
Client: testAPIClient,
Account: testAccount,
FullTransparentMode: false,
}).Start()
// wait for the API to start
if err := pingAPI(testURL(pingEndpoint), 5); err != nil {
panic(err)
}
// run the tests
os.Exit(m.Run())
}
4 changes: 4 additions & 0 deletions api/routes.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package api

const (
// ping route
// GET /ping to check the server status
pingEndpoint = "/ping"

// auth routes

// POST /auth/refresh to refresh the JWT token
Expand Down
145 changes: 145 additions & 0 deletions api/users_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package api

import (
"bytes"
"io"
"net/http"
"strings"
"testing"

qt "github.com/frankban/quicktest"
)

func TestRegisterHandler(t *testing.T) {
c := qt.New(t)
defer func() {
if err := testDB.Reset(); err != nil {
c.Logf("error resetting test database: %v", err)
}
}()

registerURL := testURL(usersEndpoint)
testCases := []apiTestCase{
{
uri: registerURL,
method: http.MethodPost,
body: []byte("invalid body"),
expectedStatus: http.StatusBadRequest,
expectedBody: mustMarshal(ErrMalformedBody),
},
{
uri: registerURL,
method: http.MethodPost,
body: mustMarshal(&UserInfo{
Email: "[email protected]",
Password: "password",
FirstName: "first",
LastName: "last",
}),
expectedStatus: http.StatusOK,
},
{
uri: registerURL,
method: http.MethodPost,
body: mustMarshal(&UserInfo{
Email: "[email protected]",
Password: "password",
FirstName: "first",
LastName: "last",
}),
expectedStatus: http.StatusInternalServerError,
expectedBody: mustMarshal(ErrGenericInternalServerError),
},
{
uri: registerURL,
method: http.MethodPost,
body: mustMarshal(&UserInfo{
Email: "[email protected]",
Password: "password",
FirstName: "first",
LastName: "",
}),
expectedStatus: http.StatusBadRequest,
expectedBody: mustMarshal(ErrMalformedBody.Withf("last name is empty")),
},
{
uri: registerURL,
method: http.MethodPost,
body: mustMarshal(&UserInfo{
Email: "[email protected]",
Password: "password",
FirstName: "",
LastName: "last",
}),
expectedStatus: http.StatusBadRequest,
expectedBody: mustMarshal(ErrMalformedBody.Withf("first name is empty")),
},
{
uri: registerURL,
method: http.MethodPost,
body: mustMarshal(&UserInfo{
Email: "invalid",
Password: "password",
FirstName: "first",
LastName: "last",
}),
expectedStatus: http.StatusBadRequest,
expectedBody: mustMarshal(ErrEmailMalformed),
},
{
uri: registerURL,
method: http.MethodPost,
body: mustMarshal(&UserInfo{
Email: "",
Password: "password",
FirstName: "first",
LastName: "last",
}),
expectedStatus: http.StatusBadRequest,
expectedBody: mustMarshal(ErrEmailMalformed),
},
{
uri: registerURL,
method: http.MethodPost,
body: mustMarshal(&UserInfo{
Email: "[email protected]",
Password: "short",
FirstName: "first",
LastName: "last",
}),
expectedStatus: http.StatusBadRequest,
expectedBody: mustMarshal(ErrPasswordTooShort),
},
{
uri: registerURL,
method: http.MethodPost,
body: mustMarshal(&UserInfo{
Email: "[email protected]",
Password: "",
FirstName: "first",
LastName: "last",
}),
expectedStatus: http.StatusBadRequest,
},
}

for _, testCase := range testCases {
req, err := http.NewRequest(testCase.method, testCase.uri, bytes.NewBuffer(testCase.body))
c.Assert(err, qt.IsNil)

resp, err := http.DefaultClient.Do(req)
c.Assert(err, qt.IsNil)
defer func() {
if err := resp.Body.Close(); err != nil {
c.Errorf("error closing response body: %v", err)
}
}()

c.Assert(resp.StatusCode, qt.Equals, testCase.expectedStatus)
if testCase.expectedBody != nil {
body, err := io.ReadAll(resp.Body)
c.Assert(err, qt.IsNil)
c.Assert(strings.TrimSpace(string(body)), qt.Equals, string(testCase.expectedBody))
}
}
}
4 changes: 1 addition & 3 deletions db/mongo.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,7 @@ func New(url, database string) (*MongoStorage, error) {
// if reset flag is enabled, Reset drops the database documents and recreates indexes
// else, just init collections and create indexes
if reset := os.Getenv("VOCDONI_MONGO_RESET_DB"); reset != "" {
log.Info("resetting database")
err := ms.Reset()
if err != nil {
if err := ms.Reset(); err != nil {
return nil, err
}
} else {
Expand Down
Loading

0 comments on commit 9b1bb20

Please sign in to comment.