Skip to content

feat: add support packages for end-to-end testing #2021

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

Merged
merged 3 commits into from
May 13, 2025
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
67 changes: 67 additions & 0 deletions internal/e2e/e2e.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Package e2e provides a few utilities for use in unit tests.
package e2e

import (
"path/filepath"
"runtime"
"testing"

"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/storage"
"github.com/supabase/auth/internal/storage/test"
)

var (
projectRoot string
configPath string
)

func init() {
if testing.Testing() {
_, thisFile, _, _ := runtime.Caller(0)
projectRoot = filepath.Join(filepath.Dir(thisFile), "../..")
configPath = filepath.Join(GetProjectRoot(), "hack", "test.env")
} else {
panic("package e2e may not be used in a main package")
}
}

// GetProjectRoot returns the path to the root of the project. This may be used
// to locate files without needing the relative path from a given test.
func GetProjectRoot() string {
return projectRoot
}

// GetConfigPath returns the path for the "/hack/test.env" config file.
func GetConfigPath() string {
return configPath
}

// Config calls conf.LoadGlobal using GetConfigPath().
func Config() (*conf.GlobalConfiguration, error) {
globalCfg, err := conf.LoadGlobal(GetConfigPath())
if err != nil {
return nil, err
}
return globalCfg, nil
}

// Conn returns a connection for the given config.
func Conn(globalCfg *conf.GlobalConfiguration) (*storage.Connection, error) {
conn, err := test.SetupDBConnection(globalCfg)
if err != nil {
return nil, err
}
return conn, nil
}

// Must may be used by Config and Conn, i.e.:
//
// cfg := e2e.Must(e2e.Config())
// conn := e2e.Must(e2e.Conn(cfg))
func Must[T any](res T, err error) T {
if err != nil {
panic(err)
}
return res
}
97 changes: 97 additions & 0 deletions internal/e2e/e2e_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package e2e

import (
"errors"
"testing"

"github.com/stretchr/testify/require"
"github.com/supabase/auth/internal/conf"
)

func TestUtils(t *testing.T) {

// check paths
require.Equal(t, projectRoot, GetProjectRoot())
require.Equal(t, configPath, GetConfigPath())

// Config
func() {

// positive
{
testCfgPath := "../../hack/test.env"
testCfg := Must(conf.LoadGlobal(testCfgPath))
globalCfg := Must(Config())
require.Equal(t, testCfg, globalCfg)
}

// negative
{
restore := configPath
defer func() {
configPath = restore
}()
configPath = "abc"

globalCfg, err := Config()
if err == nil {
t.Fatal("exp non-nil err")
}
if globalCfg != nil {
t.Fatal("exp nil conn")
}
}
}()

// Conn
func() {
// positive
{
globalCfg := Must(Config())
conn := Must(Conn(globalCfg))
if conn == nil {
t.Fatal("exp non-nil conn")
}
}

// negative
{
globalCfg := Must(Config())
globalCfg.DB.Driver = ""
globalCfg.DB.URL = "invalid"
conn, err := Conn(globalCfg)
if err == nil {
t.Fatal("exp non-nil err")
}
if conn != nil {
t.Fatal("exp nil conn")
}
}

}()

// Must
func() {
restore := configPath
defer func() {
configPath = restore
}()
configPath = "abc"

var err error
func() {
defer func() {
err = recover().(error)
}()

globalCfg := Must(Config())
if globalCfg != nil {
panic(errors.New("globalCfg != nil"))
}
}()

if err == nil {
t.Fatal("exp non-nil err")
}
}()
}
151 changes: 151 additions & 0 deletions internal/e2e/e2eapi/e2eapi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Package e2eapi provides utilities for end-to-end testing the api.
package e2eapi

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"

"github.com/supabase/auth/internal/api"
"github.com/supabase/auth/internal/api/apierrors"
"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/storage"
"github.com/supabase/auth/internal/storage/test"
"github.com/supabase/auth/internal/utilities"
)

type Instance struct {
Config *conf.GlobalConfiguration
Conn *storage.Connection
APIServer *httptest.Server

closers []func()
}

func New(globalCfg *conf.GlobalConfiguration) (*Instance, error) {
o := new(Instance)
o.Config = globalCfg

conn, err := test.SetupDBConnection(globalCfg)
if err != nil {
return nil, fmt.Errorf("error setting up db connection: %w", err)
}
o.addCloser(func() {
if conn.Store != nil {
_ = conn.Close()
}
})
o.Conn = conn

apiVer := utilities.Version
if apiVer == "" {
apiVer = "1"
}

a := api.NewAPIWithVersion(globalCfg, conn, apiVer)
apiSrv := httptest.NewServer(a)
o.addCloser(apiSrv)
o.APIServer = apiSrv

return o, nil
}

func (o *Instance) Close() error {
for _, fn := range o.closers {
defer fn()
}
return nil
}

func (o *Instance) addCloser(v any) {
switch T := any(v).(type) {
case func():
o.closers = append(o.closers, T)
case interface{ Close() }:
o.closers = append(o.closers, func() { T.Close() })
}
}

func Do(
ctx context.Context,
method string,
url string,
req, res any,
) error {
var rdr io.Reader
if req != nil {
buf := new(bytes.Buffer)
err := json.NewEncoder(buf).Encode(req)
if err != nil {
return err
}
rdr = buf
}

data, err := do(ctx, method, url, rdr)
if err != nil {
return err
}
if err := json.Unmarshal(data, res); err != nil {
return err
}
return nil
}

func do(
ctx context.Context,
method string,
url string,
body io.Reader,
) ([]byte, error) {
httpReq, err := http.NewRequestWithContext(ctx, method, url, body)
if err != nil {
return nil, err
}

h := httpReq.Header
h.Add("X-Client-Info", "auth-go/v1.0.0")
h.Add("X-Supabase-Api-Version", "2024-01-01")
h.Add("Content-Type", "application/json")
h.Add("Accept", "application/json")

httpRes, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, err
}
defer httpRes.Body.Close()

switch sc := httpRes.StatusCode; {
case sc == http.StatusNoContent:
return nil, nil

case sc >= 400:
data, err := io.ReadAll(io.LimitReader(httpRes.Body, 1e8))
if err != nil {
return nil, err
}

apiErr := new(api.HTTPErrorResponse20240101)
if err := json.Unmarshal(data, apiErr); err != nil {
return nil, err
}

err = &apierrors.HTTPError{
HTTPStatus: sc,
ErrorCode: apiErr.Code,
Message: apiErr.Message,
}
return nil, err

default:
data, err := io.ReadAll(io.LimitReader(httpRes.Body, 1e8))
if err != nil {
return nil, err
}
return data, nil
}
}
Loading
Loading