chatdesk-ui/auth_v2.169.0/internal/api/hooks_test.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")
}