diff --git a/app/api/answers/submit.feature b/app/api/answers/submit.feature index 4ab60a1e9..38743eef5 100644 --- a/app/api/answers/submit.feature +++ b/app/api/answers/submit.feature @@ -73,6 +73,10 @@ Feature: Submit a new answer "success": true } """ + And logs should contain: + """ + user_id=101 + """ And the table "answers" should be: | author_id | participant_id | attempt_id | item_id | type | answer | ABS(TIMESTAMPDIFF(SECOND, created_at, NOW())) < 3 | | 101 | 101 | 1 | 50 | Submission | print 1 | 1 | @@ -125,6 +129,10 @@ Feature: Submit a new answer "success": true } """ + And logs should contain: + """ + user_id=101 + """ And the table "answers" should be: | author_id | participant_id | attempt_id | item_id | type | answer | ABS(TIMESTAMPDIFF(SECOND, created_at, NOW())) < 3 | | 101 | 201 | 1 | 50 | Submission | print 1 | 1 | @@ -181,6 +189,10 @@ Feature: Submit a new answer "success": true } """ + And logs should contain: + """ + user_id=101 + """ And the table "answers" should be: | author_id | participant_id | attempt_id | item_id | type | answer | ABS(TIMESTAMPDIFF(SECOND, created_at, NOW())) < 3 | | 101 | 101 | 1 | 50 | Submission | print(2) | 1 | diff --git a/app/api/answers/submit.go b/app/api/answers/submit.go index 3c228f85c..915c7515e 100644 --- a/app/api/answers/submit.go +++ b/app/api/answers/submit.go @@ -13,6 +13,7 @@ import ( "github.com/France-ioi/AlgoreaBackend/v2/app/database" "github.com/France-ioi/AlgoreaBackend/v2/app/doc" + "github.com/France-ioi/AlgoreaBackend/v2/app/logging" "github.com/France-ioi/AlgoreaBackend/v2/app/service" "github.com/France-ioi/AlgoreaBackend/v2/app/token" ) @@ -95,6 +96,8 @@ func (srv *Service) submit(rw http.ResponseWriter, httpReq *http.Request) servic var hintsInfo *database.HintsInfo apiError := service.NoError + logging.LogEntrySetField(httpReq, "user_id", requestData.TaskToken.Converted.UserID) + err = srv.GetStore(httpReq).InTransaction(func(store *database.DataStore) error { var hasAccess bool var reason error diff --git a/app/api/auth/create_access_token.go b/app/api/auth/create_access_token.go index 7f31e9769..89b8a29b4 100644 --- a/app/api/auth/create_access_token.go +++ b/app/api/auth/create_access_token.go @@ -19,6 +19,7 @@ import ( "github.com/France-ioi/AlgoreaBackend/v2/app/auth" "github.com/France-ioi/AlgoreaBackend/v2/app/database" "github.com/France-ioi/AlgoreaBackend/v2/app/domain" + "github.com/France-ioi/AlgoreaBackend/v2/app/logging" "github.com/France-ioi/AlgoreaBackend/v2/app/loginmodule" "github.com/France-ioi/AlgoreaBackend/v2/app/rand" "github.com/France-ioi/AlgoreaBackend/v2/app/service" @@ -268,6 +269,7 @@ func (srv *Service) createAccessToken(w http.ResponseWriter, r *http.Request) se service.MustNotBeError(srv.GetStore(r).InTransaction(func(store *database.DataStore) error { userID := createOrUpdateUser(store.Users(), userProfile, domainConfig) + logging.LogEntrySetField(r, "user_id", userID) service.MustNotBeError(store.Groups().StoreBadges(userProfile["badges"].([]database.Badge), userID, true)) sessionID := rand.Int63() diff --git a/app/api/auth/create_access_token.robustness.feature b/app/api/auth/create_access_token.robustness.feature index dd97bfc21..6b1a19b29 100644 --- a/app/api/auth/create_access_token.robustness.feature +++ b/app/api/auth/create_access_token.robustness.feature @@ -120,7 +120,7 @@ Feature: Login callback - robustness And the response error message should contain "Can't retrieve user's profile (status code = 500)" And logs should contain: """ - Can't retrieve user's profile (status code = 500, response = "Unknown error") + {{ quote(`Can't retrieve user's profile (status code = 500, response = "Unknown error")`) }} """ And the table "users" should stay unchanged And the table "groups" should stay unchanged @@ -149,7 +149,7 @@ Feature: Login callback - robustness And the response error message should contain "Can't parse user's profile" And logs should contain: """ - Can't parse user's profile (response = "Not a JSON", error = "invalid character 'N' looking for beginning of value") + {{ quote(`Can't parse user's profile (response = "Not a JSON", error = "invalid character 'N' looking for beginning of value")`)}} """ And the table "users" should stay unchanged And the table "groups" should stay unchanged @@ -178,7 +178,7 @@ Feature: Login callback - robustness And the response error message should contain "User's profile is invalid" And logs should contain: """ - User's profile is invalid (response = "{{``|safeJs}}", error = "") + {{ quote(`User's profile is invalid (response = ` + quote(``) + `, error = "")`) }} """ And the table "users" should stay unchanged And the table "groups" should stay unchanged diff --git a/app/api/items/ask_hint.feature b/app/api/items/ask_hint.feature index 59382d44f..117824a7b 100644 --- a/app/api/items/ask_hint.feature +++ b/app/api/items/ask_hint.feature @@ -345,5 +345,5 @@ Feature: Ask for a hint And the table "results_propagate" should be empty And logs should contain: """ - Unable to parse hints_requested ({"idAttempt":"101/0","idItemLocal":"50","idUser":"101"}) having value "not an array": invalid character 'o' in literal null (expecting 'u') + {{ quote(`Unable to parse hints_requested ({"idAttempt":"101/0","idItemLocal":"50","idUser":"101"}) having value "not an array": invalid character 'o' in literal null (expecting 'u')`) }} """ diff --git a/app/api/items/ask_hint.go b/app/api/items/ask_hint.go index 370db5667..9768a9b60 100644 --- a/app/api/items/ask_hint.go +++ b/app/api/items/ask_hint.go @@ -101,6 +101,8 @@ func (srv *Service) askHint(w http.ResponseWriter, r *http.Request) service.APIE return apiError } + logging.LogEntrySetField(r, "user_id", requestData.TaskToken.Converted.UserID) + err = store.InTransaction(func(store *database.DataStore) error { var hasAccess bool var reason error diff --git a/app/api/items/save_grade.go b/app/api/items/save_grade.go index f9f9265f5..44026829b 100644 --- a/app/api/items/save_grade.go +++ b/app/api/items/save_grade.go @@ -112,6 +112,8 @@ func (srv *Service) saveGrade(w http.ResponseWriter, r *http.Request) service.AP return service.ErrInvalidRequest(err) } + logging.LogEntrySetField(r, "user_id", requestData.ScoreToken.Converted.UserID) + var validated, ok bool unlockedItems := make([]map[string]interface{}, 0) err = store.InTransaction(func(store *database.DataStore) error { @@ -133,7 +135,7 @@ func (srv *Service) saveGrade(w http.ResponseWriter, r *http.Request) service.AP ON default_strings.item_id = items.id AND default_strings.language_tag = items.default_language_tag`). Joins(`LEFT JOIN items_strings user_strings ON user_strings.item_id=items.id AND user_strings.language_tag = (SELECT default_language FROM users WHERE group_id = ?)`, - requestData.ScoreToken.UserID). + requestData.ScoreToken.Converted.UserID). Where("items.id IN (?)", unlockedItemIDs.Values()). Order("items.id"). ScanIntoSliceOfMaps(&unlockedItems).Error()) diff --git a/app/api/items/save_grade.robustness.feature b/app/api/items/save_grade.robustness.feature index f8706b28a..2f995e3aa 100644 --- a/app/api/items/save_grade.robustness.feature +++ b/app/api/items/save_grade.robustness.feature @@ -299,7 +299,7 @@ Feature: Save grading result - robustness And the response error message should contain "The answer has been already graded or is not found" And logs should contain: """ - A user tries to replay a score token with a different score value ({"idAttempt":"101/1","idItem":"80","idUser":"101","idUserAnswer":"124","newScore":100,"oldScore":0}) + {{ quote(`A user tries to replay a score token with a different score value ({"idAttempt":"101/1","idItem":"80","idUser":"101","idUserAnswer":"124","newScore":100,"oldScore":0})`) }} """ And the table "answers" should stay unchanged And the table "attempts" should stay unchanged diff --git a/app/auth/middleware.go b/app/auth/middleware.go index 1d7a1b408..a11414afa 100644 --- a/app/auth/middleware.go +++ b/app/auth/middleware.go @@ -98,6 +98,8 @@ func ValidatesUserAuthentication(service GetStorer, w http.ResponseWriter, r *ht ctx = context.WithValue(ctx, ctxUser, &user) ctx = context.WithValue(ctx, ctxSessionID, sessionID) + logging.LogEntrySetField(r, "user_id", user.GroupID) + return ctx, true, "", nil } diff --git a/app/auth/middleware_test.go b/app/auth/middleware_test.go index b990da971..d31697bce 100644 --- a/app/auth/middleware_test.go +++ b/app/auth/middleware_test.go @@ -47,6 +47,7 @@ func TestUserMiddleware(t *testing.T) { expectedStatusCode: 200, expectedServiceWasCalled: true, expectedBody: "user_id:890123\nBearer:1234567", + expectedLogs: "user_id=890123", }, { name: "missing access token", @@ -62,7 +63,7 @@ func TestUserMiddleware(t *testing.T) { expectedStatusCode: 500, expectedServiceWasCalled: false, expectedBody: `{"success":false,"message":"Internal server error"}` + "\n", - expectedLogs: `level=error msg="Can't validate an access token: some error"`, + expectedLogs: `level=error .* msg="Can't validate an access token: some error"`, }, { name: "expired token", @@ -117,6 +118,7 @@ func TestUserMiddleware(t *testing.T) { expectedStatusCode: 200, expectedServiceWasCalled: true, expectedBody: "user_id:890123", + expectedLogs: "user_id=890123", }, { name: "accepts access token from cookies", @@ -126,6 +128,7 @@ func TestUserMiddleware(t *testing.T) { expectedStatusCode: 200, expectedServiceWasCalled: true, expectedBody: "user_id:890123", + expectedLogs: "user_id=890123", }, { name: "takes the first access token from cookies", @@ -138,6 +141,7 @@ func TestUserMiddleware(t *testing.T) { expectedStatusCode: 200, expectedServiceWasCalled: true, expectedBody: "user_id:890123", + expectedLogs: "user_id=890123", }, { name: "prefers an access token from the Authorization header if both cookie and the Authorization header are given", @@ -148,6 +152,7 @@ func TestUserMiddleware(t *testing.T) { expectedStatusCode: 200, expectedServiceWasCalled: true, expectedBody: "user_id:890123", + expectedLogs: "user_id=890123", }, { name: "sets user attributes", @@ -217,10 +222,8 @@ func TestUserMiddleware(t *testing.T) { } assert.Contains(string(bodyBytes), tt.expectedBody) logs := (&loggingtest.Hook{Hook: logHook}).GetAllStructuredLogs() - if tt.expectedLogs == "" { - assert.Empty(logs) - } else { - assert.Contains(logs, tt.expectedLogs) + if tt.expectedLogs != "" { + assert.Regexp(tt.expectedLogs, logs) } assert.NoError(mock.ExpectationsWereMet()) }) @@ -281,7 +284,7 @@ func callAuthThroughMiddleware(expectedAccessToken string, authorizationHeaders, w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(body)) }) - mainSrv := httptest.NewServer(middleware(handler)) + mainSrv := httptest.NewServer(logging.NewStructuredLogger()(middleware(handler))) defer mainSrv.Close() // calling web server diff --git a/app/database/db_test.go b/app/database/db_test.go index 61d297011..33c0941c6 100644 --- a/app/database/db_test.go +++ b/app/database/db_test.go @@ -206,7 +206,7 @@ func TestDB_inTransaction_RetriesOnDeadlockAndLockWaitTimeoutErrors(t *testing.T assert.NoError(t, mock.ExpectationsWereMet()) logs := (&loggingtest.Hook{Hook: logHook}).GetAllStructuredLogs() - assert.Contains(t, logs, fmt.Sprintf("Retrying transaction (count: 1) after Error %d: ", errorNumber)) + assert.Contains(t, logs, fmt.Sprintf("Retrying transaction (count: 1) after Error %d:", errorNumber)) }) } } diff --git a/app/logging/text_formatter.go b/app/logging/text_formatter.go index 404db3eff..61c399db8 100644 --- a/app/logging/text_formatter.go +++ b/app/logging/text_formatter.go @@ -23,6 +23,11 @@ func newTextFormatter(useColors bool) *textFormatter { }} } +// NewTextFormatterForTests creates a new text formatter without colors. +func NewTextFormatterForTests() logrus.Formatter { + return newTextFormatter(false) +} + var spacesRegexp = regexp.MustCompile(`\s+`) func (f *textFormatter) Format(entry *logrus.Entry) ([]byte, error) { diff --git a/app/loggingtest/loggingtest.go b/app/loggingtest/loggingtest.go index 4c8ccffb1..cedd4a265 100644 --- a/app/loggingtest/loggingtest.go +++ b/app/loggingtest/loggingtest.go @@ -7,6 +7,8 @@ import ( "strings" "github.com/sirupsen/logrus/hooks/test" //nolint + + "github.com/France-ioi/AlgoreaBackend/v2/app/logging" ) // Hook is a hook designed for dealing with logs in test scenarios. It wraps logrus/hooks/test.Hook. @@ -34,16 +36,17 @@ func (hook *Hook) GetAllLogs() string { // GetAllStructuredLogs returns all the structured logs collected by the hook as a string. func (hook *Hook) GetAllStructuredLogs() string { logs := "" + formatter := logging.NewTextFormatterForTests() for _, entry := range hook.AllEntries() { if len(logs) > 0 { logs += newLine } - logString, err := entry.String() + logBytes, err := formatter.Format(entry) if err != nil { logs += strings.TrimSpace(err.Error()) } else { - logs += strings.TrimSpace(logString) + logs += strings.TrimSpace(string(logBytes)) } } diff --git a/app/loginmodule/client_test.go b/app/loginmodule/client_test.go index 5253d5dd0..5c51b02c0 100644 --- a/app/loginmodule/client_test.go +++ b/app/loginmodule/client_test.go @@ -10,6 +10,7 @@ import ( "fmt" "net/http" "net/url" + "regexp" "runtime" "strings" "testing" @@ -165,24 +166,24 @@ func TestClient_GetUserProfile(t *testing.T) { responseCode: 200, response: "{", expectedErr: errors.New("can't parse user's profile"), - expectedLog: `level=warning msg="Can't parse user's profile (response = \"{\", error = \"unexpected EOF\")"`, + expectedLog: `level=warning .* ` + regexp.QuoteMeta(`msg="Can't parse user's profile (response = \"{\", error = \"unexpected EOF\")"`), }, { name: "invalid profile", responseCode: 200, response: "{}", expectedErr: errors.New("user's profile is invalid"), - expectedLog: `level=warning msg="User's profile is invalid (response = \"{}\", ` + - `error = \"no id in user's profile\")"`, + expectedLog: `level=warning .* ` + regexp.QuoteMeta(`msg="User's profile is invalid (response = \"{}\", `+ + `error = \"no id in user's profile\")"`), }, { name: "invalid badges", responseCode: 200, response: `{"id":100000001,"login":"jane","badges":1234}`, expectedErr: errors.New("user's profile is invalid"), - expectedLog: `level=warning msg="User's profile is invalid ` + - `(response = \"{\\\"id\\\":100000001,\\\"login\\\":\\\"jane\\\",\\\"badges\\\":1234}\", ` + - `error = \"invalid badges data\")"`, + expectedLog: `level=warning .* ` + regexp.QuoteMeta(`msg="User's profile is invalid `+ + `(response = \"{\\\"id\\\":100000001,\\\"login\\\":\\\"jane\\\",\\\"badges\\\":1234}\", `+ + `error = \"invalid badges data\")"`), }, } @@ -206,7 +207,7 @@ func TestClient_GetUserProfile(t *testing.T) { assert.Equal(t, tt.expectedErr, err) assert.Equal(t, tt.expectedProfile, gotProfile) if tt.expectedLog != "" { - assert.Contains(t, (&loggingtest.Hook{Hook: hook}).GetAllStructuredLogs(), tt.expectedLog) + assert.Regexp(t, tt.expectedLog, (&loggingtest.Hook{Hook: hook}).GetAllStructuredLogs()) } assert.NoError(t, httpmock.AllStubsCalled()) }) @@ -440,33 +441,33 @@ func TestClient_AccountsManagerEndpoints(t *testing.T) { responseCode: 500, response: "Unexpected error", expectedErr: fmt.Errorf(testSuite.errorMessage+": %s", "bad response code"), - expectedLog: `level=warning msg="Login module returned a bad status code for /platform_api/` + - testSuite.endpoint + ` (status code = 500, response = \"Unexpected error\")"`, + expectedLog: `level=warning .* ` + regexp.QuoteMeta(`msg="Login module returned a bad status code for /platform_api/`+ + testSuite.endpoint+` (status code = 500, response = \"Unexpected error\")"`), }, { name: "corrupted base64", responseCode: 200, response: "Some text", expectedErr: fmt.Errorf(testSuite.errorMessage+": %s", "illegal base64 data at input byte 4"), - expectedLog: `level=warning msg="Can't decode response from the login module for /platform_api/` + - testSuite.endpoint + ` (status code = 200, response = \"Some text\"): illegal base64 data at input byte 4"`, + expectedLog: `level=warning .*` + regexp.QuoteMeta(`msg="Can't decode response from the login module for /platform_api/`+ + testSuite.endpoint+` (status code = 200, response = \"Some text\"): illegal base64 data at input byte 4"`), }, { name: "can't unmarshal", responseCode: 200, response: encodeAccountsManagerResponse(`{"success":true}`, "anotherClientKey"), expectedErr: fmt.Errorf(testSuite.errorMessage+": %s", "invalid character 'Ý' in literal true (expecting 'r')"), - expectedLog: `level=warning msg="Can't parse response from the login module for /platform_api/` + - testSuite.endpoint + - ` (decrypted response = \"t\\xdd\\t\\xc0\\x02\\xe9M.{0\\xa5\\xba\\xff\\xcb@|\", ` + - `encrypted response = \"K\\f_Bd\\xa5et\\xa5̡\\xfa蠐x\"): invalid character 'Ý' in literal true (expecting 'r')"`, + expectedLog: `level=warning .*` + regexp.QuoteMeta(`msg="Can't parse response from the login module for /platform_api/`+ + testSuite.endpoint+ + ` (decrypted response = \"t\\xdd\\t\\xc0\\x02\\xe9M.{0\\xa5\\xba\\xff\\xcb@|\", `+ + `encrypted response = \"K\\f_Bd\\xa5et\\xa5̡\\xfa蠐x\"): invalid character 'Ý' in literal true (expecting 'r')"`), }, { name: "'success' is false", responseCode: 200, response: encodeAccountsManagerResponse(`{"error":"unknown error"}`, "clientKeyclientKey"), - expectedLog: `level=warning msg="The login module returned an error for /platform_api/` + - testSuite.endpoint + `: unknown error"`, + expectedLog: `level=warning .*` + regexp.QuoteMeta(`msg="The login module returned an error for /platform_api/`+ + testSuite.endpoint+`: unknown error"`), }, } const moduleURL = "http://login.url.com" @@ -500,7 +501,7 @@ func TestClient_AccountsManagerEndpoints(t *testing.T) { assert.Equal(t, tt.expectedResult, result) assert.Equal(t, tt.expectedErr, err) if tt.expectedLog != "" { - assert.Contains(t, (&loggingtest.Hook{Hook: hook}).GetAllStructuredLogs(), tt.expectedLog) + assert.Regexp(t, tt.expectedLog, (&loggingtest.Hook{Hook: hook}).GetAllStructuredLogs()) } assert.NoError(t, httpmock.AllStubsCalled()) }) @@ -572,31 +573,35 @@ func TestClient_CreateUsers(t *testing.T) { responseCode: 500, response: "Unexpected error", expectedErr: fmt.Errorf("can't create users: %s", "bad response code"), - expectedLog: `level=warning msg="Login module returned a bad status code for /platform_api/accounts_manager/create ` + - `(status code = 500, response = \"Unexpected error\")"`, + expectedLog: `level=warning .* ` + + regexp.QuoteMeta(`msg="Login module returned a bad status code for /platform_api/accounts_manager/create `+ + `(status code = 500, response = \"Unexpected error\")"`), }, { name: "corrupted base64", responseCode: 200, response: "Some text", expectedErr: fmt.Errorf("can't create users: %s", "illegal base64 data at input byte 4"), - expectedLog: `level=warning msg="Can't decode response from the login module for /platform_api/accounts_manager/create ` + - `(status code = 200, response = \"Some text\"): illegal base64 data at input byte 4"`, + expectedLog: `level=warning .* ` + + regexp.QuoteMeta(`msg="Can't decode response from the login module for /platform_api/accounts_manager/create `+ + `(status code = 200, response = \"Some text\"): illegal base64 data at input byte 4"`), }, { name: "can't unmarshal", responseCode: 200, response: encodeAccountsManagerResponse(`{"success":true}`, "anotherClientKey"), expectedErr: fmt.Errorf("can't create users: %s", "invalid character 'Ý' in literal true (expecting 'r')"), - expectedLog: `level=warning msg="Can't parse response from the login module for /platform_api/accounts_manager/create ` + - `(decrypted response = \"t\\xdd\\t\\xc0\\x02\\xe9M.{0\\xa5\\xba\\xff\\xcb@|\", ` + - `encrypted response = \"K\\f_Bd\\xa5et\\xa5̡\\xfa蠐x\"): invalid character 'Ý' in literal true (expecting 'r')"`, + expectedLog: `level=warning .* ` + + regexp.QuoteMeta(`msg="Can't parse response from the login module for /platform_api/accounts_manager/create `+ + `(decrypted response = \"t\\xdd\\t\\xc0\\x02\\xe9M.{0\\xa5\\xba\\xff\\xcb@|\", `+ + `encrypted response = \"K\\f_Bd\\xa5et\\xa5̡\\xfa蠐x\"): invalid character 'Ý' in literal true (expecting 'r')"`), }, { name: "'success' is false", responseCode: 200, response: encodeAccountsManagerResponse(`{"error":"unknown error"}`, "clientKeyclientKey"), - expectedLog: `level=warning msg="The login module returned an error for /platform_api/accounts_manager/create: unknown error"`, + expectedLog: `level=warning .* ` + + regexp.QuoteMeta(`msg="The login module returned an error for /platform_api/accounts_manager/create: unknown error"`), }, } @@ -637,7 +642,7 @@ func TestClient_CreateUsers(t *testing.T) { assert.Equal(t, tt.expectedErr, err) assert.Equal(t, tt.expectedData, data) if tt.expectedLog != "" { - assert.Contains(t, (&loggingtest.Hook{Hook: hook}).GetAllStructuredLogs(), tt.expectedLog) + assert.Regexp(t, tt.expectedLog, (&loggingtest.Hook{Hook: hook}).GetAllStructuredLogs()) } assert.NoError(t, httpmock.AllStubsCalled()) }) diff --git a/app/service/propagation_integration_test.go b/app/service/propagation_integration_test.go index ac2539e1a..0adb01c46 100644 --- a/app/service/propagation_integration_test.go +++ b/app/service/propagation_integration_test.go @@ -3,6 +3,7 @@ package service_test import ( "fmt" "net/http" + "regexp" "testing" "github.com/stretchr/testify/assert" @@ -120,7 +121,7 @@ func TestSchedulePropagation(t *testing.T) { // Verify logs. if tt.loggedError != "" { logs := (&loggingtest.Hook{Hook: logHook}).GetAllStructuredLogs() - assert.Contains(t, logs, fmt.Sprintf("level=error msg=%q", tt.loggedError)) + assert.Regexp(t, "level=error .* "+regexp.QuoteMeta(fmt.Sprintf("msg=%q", tt.loggedError)), logs) } }) } diff --git a/testhelpers/steps_misc.go b/testhelpers/steps_misc.go index 89ebcd39e..2f24c82ac 100644 --- a/testhelpers/steps_misc.go +++ b/testhelpers/steps_misc.go @@ -104,7 +104,7 @@ func (ctx *TestContext) LogsShouldContain(docString *godog.DocString) error { return err } stringToSearch := strings.TrimSpace(preprocessed) - logs := ctx.logsHook.GetAllLogs() + logs := ctx.logsHook.GetAllStructuredLogs() if !strings.Contains(logs, stringToSearch) { return fmt.Errorf("cannot find %q in logs:\n%s", stringToSearch, logs) } diff --git a/testhelpers/template.go b/testhelpers/template.go index 712b48985..04a6abadc 100644 --- a/testhelpers/template.go +++ b/testhelpers/template.go @@ -158,6 +158,11 @@ func (ctx *TestContext) constructTemplateSet() *jet.Set { set.AddGlobal("taskPlatformPublicKey", tokentest.TaskPlatformPublicKey) set.AddGlobal("taskPlatformPrivateKey", tokentest.TaskPlatformPrivateKey) + set.AddGlobalFunc("quote", func(a jet.Arguments) reflect.Value { + a.RequireNumOfArguments("quote", 1, 1) + return reflect.ValueOf(fmt.Sprintf("%q", a.Get(0).Interface())) + }) + return set }