Skip to content

Commit 7cd368e

Browse files
committed
anon questionnaires should not suport draft mode as we cannot tie it back to a specific user
1 parent 73a8ee6 commit 7cd368e

3 files changed

Lines changed: 53 additions & 88 deletions

File tree

internal/httpserve/handlers/errors.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ var (
122122
ErrAssessmentResponseAlreadyCompleted = errors.New("assessment response has already been completed")
123123
// ErrAssessmentResponseOverdue is returned when trying to access or submit a questionnaire that is past due
124124
ErrAssessmentResponseOverdue = errors.New("assessment response is overdue")
125+
// ErrAnonymousQuestionnaireDraft is returned when an anonymous questionnaire tries to save a draft
126+
ErrAnonymousQuestionnaireDraft = errors.New("anonymous questionnaire responses cannot be saved as drafts")
125127
// ErrAuthenticationRequired indicates that the user must be authenticated to perform this action
126128
ErrAuthenticationRequired = errors.New("authentication required")
127129
// ErrNoActiveImpersonationSession indicates that there is no active impersonation session

internal/httpserve/handlers/questionnaire.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,14 @@ func (h *Handler) SubmitQuestionnaire(ctx echo.Context, openapi *OpenAPIContext)
137137
email string
138138
allowCtx context.Context
139139
ownerID string
140+
isAnonymous bool
140141
)
141142

142143
allowCtx = privacy.DecisionContext(reqCtx, privacy.Allow)
143144

144145
if anonAssessmentID, ok := auth.ActiveAssessmentIDKey.Get(reqCtx); ok {
145146
assessmentID = anonAssessmentID
147+
isAnonymous = true
146148

147149
anonCaller, callerOk := auth.CallerFromContext(reqCtx)
148150
if callerOk && anonCaller != nil {
@@ -188,6 +190,10 @@ func (h *Handler) SubmitQuestionnaire(ctx echo.Context, openapi *OpenAPIContext)
188190
return h.BadRequest(ctx, ErrMissingQuestionnaireData, openapi)
189191
}
190192

193+
if isAnonymous && req.IsDraft {
194+
return h.BadRequest(ctx, ErrAnonymousQuestionnaireDraft, openapi)
195+
}
196+
191197
assessment, err := h.DBClient.Assessment.Query().
192198
Where(assessment.IDEQ(assessmentID)).
193199
WithAssessmentResponses().

internal/httpserve/handlers/questionnaire_test.go

Lines changed: 45 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ func (suite *HandlerTestSuite) TestGetQuestionnaireAlreadyCompleted() {
358358
suite.db.Template.DeleteOneID(template.ID).Exec(ctx)
359359
}
360360

361-
func (suite *HandlerTestSuite) TestGetQuestionnaireReturnsDraftData() {
361+
func (suite *HandlerTestSuite) TestAnonymousQuestionnaireRejectsDraft() {
362362
t := suite.T()
363363

364364
submitOp := suite.createImpersonationOperation("SubmitQuestionnaire", "Submit questionnaire response data")
@@ -442,10 +442,7 @@ func (suite *HandlerTestSuite) TestGetQuestionnaireReturnsDraftData() {
442442
assert.Nil(t, out.SavedData)
443443
})
444444

445-
// save a draft
446-
var documentDataID string
447-
448-
t.Run("save draft", func(t *testing.T) {
445+
t.Run("reject draft", func(t *testing.T) {
449446
draftReq := models.SubmitQuestionnaireRequest{
450447
Data: map[string]any{"q1": "Alice", "q2": "Engineer"},
451448
IsDraft: true,
@@ -461,19 +458,22 @@ func (suite *HandlerTestSuite) TestGetQuestionnaireReturnsDraftData() {
461458
recorder := httptest.NewRecorder()
462459
suite.e.ServeHTTP(recorder, req)
463460

464-
require.Equal(t, http.StatusOK, recorder.Code)
461+
require.Equal(t, http.StatusBadRequest, recorder.Code)
465462

466-
var out models.SubmitQuestionnaireResponse
467-
err = json.NewDecoder(recorder.Result().Body).Decode(&out)
463+
var errorResp map[string]interface{}
464+
err = json.NewDecoder(recorder.Result().Body).Decode(&errorResp)
468465
require.NoError(t, err)
466+
if errorMsg, ok := errorResp["error"].(string); ok {
467+
assert.Contains(t, errorMsg, "cannot be saved as drafts")
468+
}
469469

470-
assert.Equal(t, "DRAFT", out.Status)
471-
472-
documentDataID = out.DocumentDataID
470+
updatedResponse, err := suite.db.AssessmentResponse.Get(questionnaireCtx, assessmentResponse.ID)
471+
require.NoError(t, err)
472+
assert.Empty(t, updatedResponse.DocumentDataID)
473+
assert.Equal(t, enums.AssessmentResponseStatusSent, updatedResponse.Status)
473474
})
474475

475-
// fetch after draft - saved data returned alongside form config
476-
t.Run("saved data returned after draft", func(t *testing.T) {
476+
t.Run("no saved data after rejected draft", func(t *testing.T) {
477477
req := httptest.NewRequest(http.MethodGet, "/questionnaire", nil)
478478
req.Header.Set("Content-Type", "application/json")
479479
req.Header.Set("Authorization", "Bearer "+accessToken)
@@ -489,16 +489,9 @@ func (suite *HandlerTestSuite) TestGetQuestionnaireReturnsDraftData() {
489489

490490
assert.NotNil(t, out.Jsonconfig)
491491
assert.Equal(t, "Draft Fetch Test", out.Jsonconfig["title"])
492-
493-
require.NotNil(t, out.SavedData)
494-
assert.Equal(t, "Alice", out.SavedData["q1"])
495-
assert.Equal(t, "Engineer", out.SavedData["q2"])
492+
assert.Nil(t, out.SavedData)
496493
})
497494

498-
if documentDataID != "" {
499-
suite.db.DocumentData.DeleteOneID(documentDataID).Exec(questionnaireCtx)
500-
}
501-
502495
suite.db.AssessmentResponse.DeleteOneID(assessmentResponse.ID).Exec(questionnaireCtx)
503496
suite.db.Assessment.DeleteOneID(assessment.ID).Exec(questionnaireCtx)
504497
suite.db.Template.DeleteOneID(template.ID).Exec(questionnaireCtx)
@@ -995,10 +988,7 @@ func (suite *HandlerTestSuite) TestSubmitQuestionnaireDraft() {
995988
t := suite.T()
996989

997990
operation := suite.createImpersonationOperation("SubmitQuestionnaire", "Submit questionnaire response data")
998-
suite.registerAuthenticatedTestHandler("POST", "/questionnaire", operation, suite.h.SubmitQuestionnaire)
999-
1000-
getOperation := suite.createImpersonationOperation("GetQuestionnaire", "Get questionnaire template configuration")
1001-
suite.registerAuthenticatedTestHandler("GET", "/questionnaire", getOperation, suite.h.GetQuestionnaire)
991+
suite.registerTestHandler("POST", "/questionnaire", operation, suite.h.SubmitQuestionnaire)
1002992

1003993
ec := echocontext.NewTestEchoContext().Request().Context()
1004994
ctx := privacy.DecisionContext(ec, privacy.Allow)
@@ -1019,51 +1009,37 @@ func (suite *HandlerTestSuite) TestSubmitQuestionnaireDraft() {
10191009
Save(testUser1.UserCtx)
10201010
require.NoError(t, err)
10211011

1022-
testEmail := "draft@example.com"
1023-
anonUserID := fmt.Sprintf("anon_questionnaire_%s", assessment.ID)
1024-
1025-
anonUser := auth.NewQuestionnaireCaller(testUser1.OrganizationID, anonUserID, "", testEmail)
1026-
questionnaireCtx := auth.WithCaller(ctx, anonUser)
1027-
questionnaireCtx = auth.ActiveAssessmentIDKey.Set(questionnaireCtx, assessment.ID)
1028-
10291012
assessmentResponse, err := suite.db.AssessmentResponse.Create().
10301013
SetAssessmentID(assessment.ID).
1031-
SetEmail(testEmail).
1014+
SetEmail(testUser1.UserInfo.Email).
10321015
SetOwnerID(testUser1.OrganizationID).
10331016
SetStatus(enums.AssessmentResponseStatusSent).
1034-
Save(questionnaireCtx)
1017+
Save(testUser1.UserCtx)
10351018
require.NoError(t, err)
10361019

1037-
claims := &tokens.Claims{
1038-
RegisteredClaims: jwt.RegisteredClaims{
1039-
Subject: anonUserID,
1040-
},
1041-
UserID: anonUserID,
1042-
OrgID: assessment.OwnerID,
1043-
AssessmentID: assessment.ID,
1044-
Email: testEmail,
1045-
}
1046-
1047-
accessToken, _, err := suite.h.DBClient.TokenManager.CreateTokenPair(claims)
1048-
require.NoError(t, err)
1020+
reqCtx := auth.WithCaller(ctx, &auth.Caller{
1021+
SubjectID: testUser1.ID,
1022+
SubjectEmail: testUser1.UserInfo.Email,
1023+
OrganizationID: testUser1.OrganizationID,
1024+
})
10491025

10501026
var documentDataID string
10511027

10521028
t.Run("save draft", func(t *testing.T) {
10531029
draftReq := models.SubmitQuestionnaireRequest{
1054-
Data: map[string]any{"q1": "partial answer"},
1055-
IsDraft: true,
1030+
AssessmentID: assessment.ID,
1031+
Data: map[string]any{"q1": "partial answer"},
1032+
IsDraft: true,
10561033
}
10571034

10581035
bodyBytes, err := json.Marshal(draftReq)
10591036
require.NoError(t, err)
10601037

10611038
req := httptest.NewRequest(http.MethodPost, "/questionnaire", bytes.NewReader(bodyBytes))
10621039
req.Header.Set("Content-Type", "application/json")
1063-
req.Header.Set("Authorization", "Bearer "+accessToken)
10641040

10651041
recorder := httptest.NewRecorder()
1066-
suite.e.ServeHTTP(recorder, req)
1042+
suite.e.ServeHTTP(recorder, req.WithContext(reqCtx))
10671043

10681044
assert.Equal(t, http.StatusOK, recorder.Code)
10691045

@@ -1077,27 +1053,27 @@ func (suite *HandlerTestSuite) TestSubmitQuestionnaireDraft() {
10771053

10781054
documentDataID = out.DocumentDataID
10791055

1080-
updated, err := suite.db.AssessmentResponse.Get(questionnaireCtx, assessmentResponse.ID)
1056+
updated, err := suite.db.AssessmentResponse.Get(testUser1.UserCtx, assessmentResponse.ID)
10811057
require.NoError(t, err)
10821058
assert.Equal(t, enums.AssessmentResponseStatusDraft, updated.Status)
10831059
assert.Equal(t, documentDataID, updated.DocumentDataID)
10841060
})
10851061

10861062
t.Run("update draft with new data", func(t *testing.T) {
10871063
draftReq := models.SubmitQuestionnaireRequest{
1088-
Data: map[string]any{"q1": "updated partial answer", "q2": "new field"},
1089-
IsDraft: true,
1064+
AssessmentID: assessment.ID,
1065+
Data: map[string]any{"q1": "updated partial answer", "q2": "new field"},
1066+
IsDraft: true,
10901067
}
10911068

10921069
bodyBytes, err := json.Marshal(draftReq)
10931070
require.NoError(t, err)
10941071

10951072
req := httptest.NewRequest(http.MethodPost, "/questionnaire", bytes.NewReader(bodyBytes))
10961073
req.Header.Set("Content-Type", "application/json")
1097-
req.Header.Set("Authorization", "Bearer "+accessToken)
10981074

10991075
recorder := httptest.NewRecorder()
1100-
suite.e.ServeHTTP(recorder, req)
1076+
suite.e.ServeHTTP(recorder, req.WithContext(reqCtx))
11011077

11021078
assert.Equal(t, http.StatusOK, recorder.Code)
11031079

@@ -1108,45 +1084,26 @@ func (suite *HandlerTestSuite) TestSubmitQuestionnaireDraft() {
11081084
assert.Equal(t, documentDataID, out.DocumentDataID)
11091085
assert.Equal(t, "DRAFT", out.Status)
11101086

1111-
docData, err := suite.db.DocumentData.Get(questionnaireCtx, documentDataID)
1087+
docData, err := suite.db.DocumentData.Get(testUser1.UserCtx, documentDataID)
11121088
require.NoError(t, err)
11131089
assert.Equal(t, "updated partial answer", docData.Data["q1"])
11141090
assert.Equal(t, "new field", docData.Data["q2"])
11151091
})
11161092

1117-
t.Run("get questionnaire returns saved draft data", func(t *testing.T) {
1118-
req := httptest.NewRequest(http.MethodGet, "/questionnaire", nil)
1119-
req.Header.Set("Content-Type", "application/json")
1120-
req.Header.Set("Authorization", "Bearer "+accessToken)
1121-
1122-
recorder := httptest.NewRecorder()
1123-
suite.e.ServeHTTP(recorder, req)
1124-
1125-
assert.Equal(t, http.StatusOK, recorder.Code)
1126-
1127-
var out models.GetQuestionnaireResponse
1128-
err = json.NewDecoder(recorder.Result().Body).Decode(&out)
1129-
require.NoError(t, err)
1130-
1131-
assert.NotNil(t, out.SavedData)
1132-
assert.Equal(t, "updated partial answer", out.SavedData["q1"])
1133-
assert.Equal(t, "new field", out.SavedData["q2"])
1134-
})
1135-
11361093
t.Run("complete from draft", func(t *testing.T) {
11371094
submitReq := models.SubmitQuestionnaireRequest{
1138-
Data: map[string]any{"q1": "final answer", "q2": "final field"},
1095+
AssessmentID: assessment.ID,
1096+
Data: map[string]any{"q1": "final answer", "q2": "final field"},
11391097
}
11401098

11411099
bodyBytes, err := json.Marshal(submitReq)
11421100
require.NoError(t, err)
11431101

11441102
req := httptest.NewRequest(http.MethodPost, "/questionnaire", bytes.NewReader(bodyBytes))
11451103
req.Header.Set("Content-Type", "application/json")
1146-
req.Header.Set("Authorization", "Bearer "+accessToken)
11471104

11481105
recorder := httptest.NewRecorder()
1149-
suite.e.ServeHTTP(recorder, req)
1106+
suite.e.ServeHTTP(recorder, req.WithContext(reqCtx))
11501107

11511108
assert.Equal(t, http.StatusOK, recorder.Code)
11521109

@@ -1158,40 +1115,40 @@ func (suite *HandlerTestSuite) TestSubmitQuestionnaireDraft() {
11581115
assert.Equal(t, "COMPLETED", out.Status)
11591116
assert.NotEmpty(t, out.CompletedAt)
11601117

1161-
updated, err := suite.db.AssessmentResponse.Get(questionnaireCtx, assessmentResponse.ID)
1118+
updated, err := suite.db.AssessmentResponse.Get(testUser1.UserCtx, assessmentResponse.ID)
11621119
require.NoError(t, err)
11631120
assert.Equal(t, enums.AssessmentResponseStatusCompleted, updated.Status)
11641121
assert.NotZero(t, updated.CompletedAt)
11651122

1166-
docData, err := suite.db.DocumentData.Get(questionnaireCtx, documentDataID)
1123+
docData, err := suite.db.DocumentData.Get(testUser1.UserCtx, documentDataID)
11671124
require.NoError(t, err)
11681125
assert.Equal(t, "final answer", docData.Data["q1"])
11691126
})
11701127

11711128
t.Run("cannot draft after completed", func(t *testing.T) {
11721129
draftReq := models.SubmitQuestionnaireRequest{
1173-
Data: map[string]any{"q1": "should fail"},
1174-
IsDraft: true,
1130+
AssessmentID: assessment.ID,
1131+
Data: map[string]any{"q1": "should fail"},
1132+
IsDraft: true,
11751133
}
11761134

11771135
bodyBytes, err := json.Marshal(draftReq)
11781136
require.NoError(t, err)
11791137

11801138
req := httptest.NewRequest(http.MethodPost, "/questionnaire", bytes.NewReader(bodyBytes))
11811139
req.Header.Set("Content-Type", "application/json")
1182-
req.Header.Set("Authorization", "Bearer "+accessToken)
11831140

11841141
recorder := httptest.NewRecorder()
1185-
suite.e.ServeHTTP(recorder, req)
1142+
suite.e.ServeHTTP(recorder, req.WithContext(reqCtx))
11861143

11871144
assert.Equal(t, http.StatusBadRequest, recorder.Code)
11881145
})
11891146

11901147
if documentDataID != "" {
1191-
suite.db.DocumentData.DeleteOneID(documentDataID).Exec(questionnaireCtx)
1148+
suite.db.DocumentData.DeleteOneID(documentDataID).Exec(testUser1.UserCtx)
11921149
}
11931150

1194-
suite.db.AssessmentResponse.DeleteOneID(assessmentResponse.ID).Exec(questionnaireCtx)
1195-
suite.db.Assessment.DeleteOneID(assessment.ID).Exec(questionnaireCtx)
1196-
suite.db.Template.DeleteOneID(template.ID).Exec(questionnaireCtx)
1151+
suite.db.AssessmentResponse.DeleteOneID(assessmentResponse.ID).Exec(testUser1.UserCtx)
1152+
suite.db.Assessment.DeleteOneID(assessment.ID).Exec(testUser1.UserCtx)
1153+
suite.db.Template.DeleteOneID(template.ID).Exec(testUser1.UserCtx)
11971154
}

0 commit comments

Comments
 (0)