288 lines
7.8 KiB
Go
288 lines
7.8 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"net/http/httptest"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/stretchr/testify/suite"
|
|
"github.com/supabase/auth/internal/conf"
|
|
"github.com/supabase/auth/internal/hooks"
|
|
"github.com/supabase/auth/internal/models"
|
|
"github.com/supabase/auth/internal/storage"
|
|
|
|
"gopkg.in/h2non/gock.v1"
|
|
)
|
|
|
|
var handleApiRequest func(*http.Request) (*http.Response, error)
|
|
|
|
type HooksTestSuite struct {
|
|
suite.Suite
|
|
API *API
|
|
Config *conf.GlobalConfiguration
|
|
TestUser *models.User
|
|
}
|
|
|
|
type MockHttpClient struct {
|
|
mock.Mock
|
|
}
|
|
|
|
func (m *MockHttpClient) Do(req *http.Request) (*http.Response, error) {
|
|
return handleApiRequest(req)
|
|
}
|
|
|
|
func TestHooks(t *testing.T) {
|
|
api, config, err := setupAPIForTest()
|
|
require.NoError(t, err)
|
|
|
|
ts := &HooksTestSuite{
|
|
API: api,
|
|
Config: config,
|
|
}
|
|
defer api.db.Close()
|
|
|
|
suite.Run(t, ts)
|
|
}
|
|
|
|
func (ts *HooksTestSuite) SetupTest() {
|
|
models.TruncateAll(ts.API.db)
|
|
u, err := models.NewUser("123456789", "testemail@gmail.com", "securetestpassword", ts.Config.JWT.Aud, nil)
|
|
require.NoError(ts.T(), err, "Error creating test user model")
|
|
require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user")
|
|
ts.TestUser = u
|
|
}
|
|
|
|
func (ts *HooksTestSuite) TestRunHTTPHook() {
|
|
// setup mock requests for hooks
|
|
defer gock.OffAll()
|
|
|
|
input := hooks.SendSMSInput{
|
|
User: ts.TestUser,
|
|
SMS: hooks.SMS{
|
|
OTP: "123456",
|
|
},
|
|
}
|
|
testURL := "http://localhost:54321/functions/v1/custom-sms-sender"
|
|
ts.Config.Hook.SendSMS.URI = testURL
|
|
|
|
unsuccessfulResponse := hooks.AuthHookError{
|
|
HTTPCode: http.StatusUnprocessableEntity,
|
|
Message: "test error",
|
|
}
|
|
|
|
testCases := []struct {
|
|
description string
|
|
expectError bool
|
|
mockResponse hooks.AuthHookError
|
|
}{
|
|
{
|
|
description: "Hook returns success",
|
|
expectError: false,
|
|
mockResponse: hooks.AuthHookError{},
|
|
},
|
|
{
|
|
description: "Hook returns error",
|
|
expectError: true,
|
|
mockResponse: unsuccessfulResponse,
|
|
},
|
|
}
|
|
|
|
gock.New(ts.Config.Hook.SendSMS.URI).
|
|
Post("/").
|
|
MatchType("json").
|
|
Reply(http.StatusOK).
|
|
JSON(hooks.SendSMSOutput{})
|
|
|
|
gock.New(ts.Config.Hook.SendSMS.URI).
|
|
Post("/").
|
|
MatchType("json").
|
|
Reply(http.StatusUnprocessableEntity).
|
|
JSON(hooks.SendSMSOutput{HookError: unsuccessfulResponse})
|
|
|
|
for _, tc := range testCases {
|
|
ts.Run(tc.description, func() {
|
|
req, _ := http.NewRequest("POST", ts.Config.Hook.SendSMS.URI, nil)
|
|
body, err := ts.API.runHTTPHook(req, ts.Config.Hook.SendSMS, &input)
|
|
|
|
if !tc.expectError {
|
|
require.NoError(ts.T(), err)
|
|
} else {
|
|
require.Error(ts.T(), err)
|
|
if body != nil {
|
|
var output hooks.SendSMSOutput
|
|
require.NoError(ts.T(), json.Unmarshal(body, &output))
|
|
require.Equal(ts.T(), unsuccessfulResponse.HTTPCode, output.HookError.HTTPCode)
|
|
require.Equal(ts.T(), unsuccessfulResponse.Message, output.HookError.Message)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
require.True(ts.T(), gock.IsDone())
|
|
}
|
|
|
|
func (ts *HooksTestSuite) TestShouldRetryWithRetryAfterHeader() {
|
|
defer gock.OffAll()
|
|
|
|
input := hooks.SendSMSInput{
|
|
User: ts.TestUser,
|
|
SMS: hooks.SMS{
|
|
OTP: "123456",
|
|
},
|
|
}
|
|
testURL := "http://localhost:54321/functions/v1/custom-sms-sender"
|
|
ts.Config.Hook.SendSMS.URI = testURL
|
|
|
|
gock.New(testURL).
|
|
Post("/").
|
|
MatchType("json").
|
|
Reply(http.StatusTooManyRequests).
|
|
SetHeader("retry-after", "true").SetHeader("content-type", "application/json")
|
|
|
|
// Simulate an additional response for the retry attempt
|
|
gock.New(testURL).
|
|
Post("/").
|
|
MatchType("json").
|
|
Reply(http.StatusOK).
|
|
JSON(hooks.SendSMSOutput{}).SetHeader("content-type", "application/json")
|
|
|
|
// Simulate the original HTTP request which triggered the hook
|
|
req, err := http.NewRequest("POST", "http://localhost:9998/otp", nil)
|
|
require.NoError(ts.T(), err)
|
|
|
|
body, err := ts.API.runHTTPHook(req, ts.Config.Hook.SendSMS, &input)
|
|
require.NoError(ts.T(), err)
|
|
|
|
var output hooks.SendSMSOutput
|
|
err = json.Unmarshal(body, &output)
|
|
require.NoError(ts.T(), err, "Unmarshal should not fail")
|
|
|
|
// Ensure that all expected HTTP interactions (mocks) have been called
|
|
require.True(ts.T(), gock.IsDone(), "Expected all mocks to have been called including retry")
|
|
}
|
|
|
|
func (ts *HooksTestSuite) TestShouldReturnErrorForNonJSONContentType() {
|
|
defer gock.OffAll()
|
|
|
|
input := hooks.SendSMSInput{
|
|
User: ts.TestUser,
|
|
SMS: hooks.SMS{
|
|
OTP: "123456",
|
|
},
|
|
}
|
|
testURL := "http://localhost:54321/functions/v1/custom-sms-sender"
|
|
ts.Config.Hook.SendSMS.URI = testURL
|
|
|
|
gock.New(testURL).
|
|
Post("/").
|
|
MatchType("json").
|
|
Reply(http.StatusOK).
|
|
SetHeader("content-type", "text/plain")
|
|
|
|
req, err := http.NewRequest("POST", "http://localhost:9999/otp", nil)
|
|
require.NoError(ts.T(), err)
|
|
|
|
_, err = ts.API.runHTTPHook(req, ts.Config.Hook.SendSMS, &input)
|
|
require.Error(ts.T(), err, "Expected an error due to wrong content type")
|
|
require.Contains(ts.T(), err.Error(), "Invalid JSON response.")
|
|
|
|
require.True(ts.T(), gock.IsDone(), "Expected all mocks to have been called")
|
|
}
|
|
|
|
func (ts *HooksTestSuite) TestInvokeHookIntegration() {
|
|
// We use the Send Email Hook as illustration
|
|
defer gock.OffAll()
|
|
hookFunctionSQL := `
|
|
create or replace function invoke_test(input jsonb)
|
|
returns json as $$
|
|
begin
|
|
return input;
|
|
end; $$ language plpgsql;`
|
|
require.NoError(ts.T(), ts.API.db.RawQuery(hookFunctionSQL).Exec())
|
|
|
|
testHTTPUri := "http://myauthservice.com/signup"
|
|
testHTTPSUri := "https://myauthservice.com/signup"
|
|
testPGUri := "pg-functions://postgres/auth/invoke_test"
|
|
successOutput := map[string]interface{}{}
|
|
authEndpoint := "https://app.myapp.com/otp"
|
|
gock.New(testHTTPUri).
|
|
Post("/").
|
|
MatchType("json").
|
|
Reply(http.StatusOK).
|
|
JSON(successOutput).SetHeader("content-type", "application/json")
|
|
|
|
gock.New(testHTTPSUri).
|
|
Post("/").
|
|
MatchType("json").
|
|
Reply(http.StatusOK).
|
|
JSON(successOutput).SetHeader("content-type", "application/json")
|
|
|
|
tests := []struct {
|
|
description string
|
|
conn *storage.Connection
|
|
request *http.Request
|
|
input any
|
|
output any
|
|
uri string
|
|
expectedError error
|
|
}{
|
|
{
|
|
description: "HTTP endpoint success",
|
|
conn: nil,
|
|
request: httptest.NewRequest("POST", authEndpoint, nil),
|
|
input: &hooks.SendEmailInput{},
|
|
output: &hooks.SendEmailOutput{},
|
|
uri: testHTTPUri,
|
|
},
|
|
{
|
|
description: "HTTPS endpoint success",
|
|
conn: nil,
|
|
request: httptest.NewRequest("POST", authEndpoint, nil),
|
|
input: &hooks.SendEmailInput{},
|
|
output: &hooks.SendEmailOutput{},
|
|
uri: testHTTPSUri,
|
|
},
|
|
{
|
|
description: "PostgreSQL function success",
|
|
conn: ts.API.db,
|
|
request: httptest.NewRequest("POST", authEndpoint, nil),
|
|
input: &hooks.SendEmailInput{},
|
|
output: &hooks.SendEmailOutput{},
|
|
uri: testPGUri,
|
|
},
|
|
{
|
|
description: "Unsupported protocol error",
|
|
conn: nil,
|
|
request: httptest.NewRequest("POST", authEndpoint, nil),
|
|
input: &hooks.SendEmailInput{},
|
|
output: &hooks.SendEmailOutput{},
|
|
uri: "ftp://example.com/path",
|
|
expectedError: errors.New("unsupported protocol: \"ftp://example.com/path\" only postgres hooks and HTTPS functions are supported at the moment"),
|
|
},
|
|
}
|
|
|
|
var err error
|
|
for _, tc := range tests {
|
|
// Set up hook config
|
|
ts.Config.Hook.SendEmail.Enabled = true
|
|
ts.Config.Hook.SendEmail.URI = tc.uri
|
|
require.NoError(ts.T(), ts.Config.Hook.SendEmail.PopulateExtensibilityPoint())
|
|
|
|
ts.Run(tc.description, func() {
|
|
err = ts.API.invokeHook(tc.conn, tc.request, tc.input, tc.output)
|
|
if tc.expectedError != nil {
|
|
require.EqualError(ts.T(), err, tc.expectedError.Error())
|
|
} else {
|
|
require.NoError(ts.T(), err)
|
|
}
|
|
})
|
|
|
|
}
|
|
// Ensure that all expected HTTP interactions (mocks) have been called
|
|
require.True(ts.T(), gock.IsDone(), "Expected all mocks to have been called including retry")
|
|
}
|