Skip to content

fix: draft response visibility#1543

Merged
kaitoyama merged 5 commits intomainfrom
codex/fix-draft-response-visibility
May 5, 2026
Merged

fix: draft response visibility#1543
kaitoyama merged 5 commits intomainfrom
codex/fix-draft-response-visibility

Conversation

@kaitoyama
Copy link
Copy Markdown
Contributor

@kaitoyama kaitoyama commented Apr 21, 2026

Summary by CodeRabbit

リリースノート

  • バグ修正

    • ドラフト回答の閲覧権限を厳格化しました。ユーザーが自身のものでないドラフト回答は閲覧できず、該当操作は権限エラー(403)になります。
  • テスト

    • ドラフト表示とアクセス制御を網羅する追加・改良されたテストを導入し、テスト間の状態共有を排除して信頼性を向上しました。

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

Warning

Rate limit exceeded

@kaitoyama has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 48 minutes and 22 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 27602d68-1617-42e9-8b18-2561c19a09fb

📥 Commits

Reviewing files that changed from the base of the PR and between 435e2d1 and e3fbdd3.

📒 Files selected for processing (2)
  • controller/questionnaire.go
  • controller/questionnaire_test.go
📝 Walkthrough

ウォークスルー

Questionnaire のレスポンス取得でドラフトアクセスの判定を局所変数 isDraft に基づいて行い、IsDraft が true の場合の権限ゲートを追加。テスト基盤を深いコピー方式に変更してテスト間の共有状態を排除し、ドラフト関連のアクセス制御テストを強化。

変更内容

コホート / ファイル サマリー
認可チェック実装
controller/questionnaire.go
GetQuestionnaireResponses ハンドラーで isDraft をローカルに算出し、isDraft == true && onlyMyResponse == false の場合に管理者権限チェック(権限なしなら 403 を返す)を行うよう変更。GetRespondentDetails には新たに算出した isDraft を渡す。
テスト基盤の改善
controller/questionnaire_test.go
テスト用質問票を一度だけ初期化する sampleQuestionnaireOnce を導入し、各テストで独立したコピーを返す newSampleQuestionnaire() を追加。テストを新関数経由で作成するように差し替え、ドラフト可視性やドラフトレスポンスのアクセス制御に関する追加ケースを含む期待値を更新。
テストケースの統一化
controller/response_test.go
複数テスト(TestGetMyResponses, TestGetResponse, TestDeleteResponse, TestEditResponse 等)で共有変数の代わりに newSampleQuestionnaire() を利用するよう変更し、各テストで個別の質問票を作成するように修正。

見積もりコード審査工数

🎯 4 (複雑) | ⏱️ ~45 分

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed このプルリクエストはドラフト応答の表示制御に関する権限チェックロジックを追加し、テストケースを拡張してアクセス制御を検証しています。タイトルは変更内容の主要な目的を明確に要約しています。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-draft-response-visibility

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 48 minutes and 22 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 21, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 63.93%. Comparing base (63f67f2) to head (e3fbdd3).
⚠️ Report is 24 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1543      +/-   ##
==========================================
+ Coverage   63.83%   63.93%   +0.10%     
==========================================
  Files          27       27              
  Lines        4040     4046       +6     
==========================================
+ Hits         2579     2587       +8     
+ Misses       1073     1071       -2     
  Partials      388      388              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@kaitoyama kaitoyama marked this pull request as ready for review April 21, 2026 03:42
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
controller/questionnaire_test.go (1)

166-181: 並列テスト向けに fixture 初期化を sync.Once で保護すると安全です。

newSampleQuestionnaire()t.Parallel() なテストから多数呼ばれるため、初回呼び出しが並行すると setupSampleQuestionnaire() の package-level 変数更新が競合し得ます。sampleQuestionnaire.Title の手動ガードより sync.Once に寄せる方が堅いです。

修正案
 import (
 	"bytes"
 	"context"
 	"encoding/json"
 	"errors"
 	"fmt"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
 	"sort"
 	"strings"
+	"sync"
 	"testing"
 	"time"
@@
 	sampleQuestionSettingsMultipleChoice = openapi.NewQuestion{}
 	sampleQeustionsettingsScale          = openapi.NewQuestion{}
 	sampleQuestionnaire                  = openapi.PostQuestionnaireJSONRequestBody{}
+	sampleQuestionnaireOnce              sync.Once
 )
 
 func setupSampleQuestionnaire() {
-	if sampleQuestionnaire.Title != "" {
-		return
-	}
+	sampleQuestionnaireOnce.Do(func() {
 	sampleQuestionSettingsText = openapi.NewQuestion{
 		Title:      "質問(テキスト)",
 		IsRequired: true,
 	}
@@
 	sampleQuestionnaire = openapi.PostQuestionnaireJSONRequestBody{
 		Admin:                    sampleAdmin,
 		Description:              "第1回集会らん☆ぷろ参加者募集",
@@
 		Target:              sampleTarget,
 		Title:               "第1回集会らん☆ぷろ募集アンケート",
 	}
+	})
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/questionnaire_test.go` around lines 166 - 181, The setup of
package-level fixtures is racy when newSampleQuestionnaire() is called from
parallel tests; change setupSampleQuestionnaire() invocation to run exactly once
using sync.Once to protect sampleQuestionnaire initialization. Add a
package-level sync.Once (e.g., var initSampleQuestionnaire sync.Once) and call
initSampleQuestionnaire.Do(setupSampleQuestionnaire) inside
newSampleQuestionnaire() before marshalling, leaving the deep-copy/unmarshal
logic intact so parallel callers get isolated copies of sampleQuestionnaire.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@controller/questionnaire_test.go`:
- Around line 2589-2618: Add a test case to cover the non-administrator default
(IsDraft == nil) path: in the test table inside questionnaire_test.go add an
entry similar to the existing "non administrator cannot view other respondents
draft responses" case but leave params.IsDraft nil (omit IsDraft) and
OnlyMyResponse set to &constFalse; assert the call to GetQuestionnaireResponses
(using args.userID userTwo and args.questionnaireID
questionnaireDetail.QuestionnaireId) returns an error or does not include the
draft response ID (response03.ResponseId) just like the explicit IsDraft=true
case, ensuring the default behavior does not leak draft IDs.

In `@controller/questionnaire.go`:
- Around line 1132-1143: The code currently skips the admin check when
params.IsDraft == nil allowing non-admins to retrieve others' drafts; update the
block around params.IsDraft/onlyMyResponse to treat a nil IsDraft as "not
explicitly requesting drafts" by performing the same admin check (use
q.IAdministrator.CheckQuestionnaireAdmin and also consider the local
adminUserIDs bypass used in controller/middleware.go), and then ensure you pass
an explicit non-nil *bool (true/false as appropriate) into GetRespondentDetails
so nil no longer implies draft-including behavior for non-admins; reference
params.IsDraft, onlyMyResponse, q.IAdministrator.CheckQuestionnaireAdmin,
GetRespondentDetails and adminUserIDs when making the change.

---

Nitpick comments:
In `@controller/questionnaire_test.go`:
- Around line 166-181: The setup of package-level fixtures is racy when
newSampleQuestionnaire() is called from parallel tests; change
setupSampleQuestionnaire() invocation to run exactly once using sync.Once to
protect sampleQuestionnaire initialization. Add a package-level sync.Once (e.g.,
var initSampleQuestionnaire sync.Once) and call
initSampleQuestionnaire.Do(setupSampleQuestionnaire) inside
newSampleQuestionnaire() before marshalling, leaving the deep-copy/unmarshal
logic intact so parallel callers get isolated copies of sampleQuestionnaire.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1e493a3a-dbe3-449a-8af7-d06260e779b3

📥 Commits

Reviewing files that changed from the base of the PR and between 1316137 and 907a134.

📒 Files selected for processing (3)
  • controller/questionnaire.go
  • controller/questionnaire_test.go
  • controller/response_test.go

Comment thread controller/questionnaire_test.go
Comment thread controller/questionnaire.go Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
controller/questionnaire.go (1)

1132-1143: ロジックは正しいですが、ネストを解消して意図を明示するとより読みやすくなります。

現状のロジックを (onlyMyResponse, params.IsDraft) の組合せで検証したところ、期待通りに動作することを確認できました(!onlyMyResponse && IsDraft==nil → 提出済みのみ、!onlyMyResponse && *IsDraft==true → 403、onlyMyResponse の場合はユーザー自身の下書きが見える)。一方で以下の点が気になります:

  1. submittedOnlyparams.IsDraft == nil のときのみ使用されるため、外側の if 内で宣言する必要がありません。ネストした if を1つにまとめると意図が明確になります。
  2. 過去レビューで提案されていた「管理者は他ユーザーの下書きを閲覧可」というバイパスが無く、テスト(L2572「administrator cannot view other respondents draft responses」)でも isErr: true を期待しているので、管理者であっても他者の下書きをこのエンドポイントから閲覧できない という仕様になっています。意図通りであれば問題ありませんが、UI側で管理者の下書き確認フローが存在する場合は別途影響確認をお願いします。
♻️ 提案する簡素化
-	isDraft := params.IsDraft
-	if !onlyMyResponse {
-		submittedOnly := false
-		if params.IsDraft == nil {
-			isDraft = &submittedOnly
-		}
-	}
-	if isDraft != nil && *isDraft && !onlyMyResponse {
+	isDraft := params.IsDraft
+	if !onlyMyResponse && isDraft == nil {
+		submittedOnly := false
+		isDraft = &submittedOnly
+	}
+	if !onlyMyResponse && isDraft != nil && *isDraft {
 		c.Logger().Infof("user %s is not allowed to view other respondents' drafts for questionnaire %d", userID, questionnaireID)
 		return res, echo.NewHTTPError(http.StatusForbidden, "you do not have permission to view other respondents' drafts")
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/questionnaire.go` around lines 1132 - 1143, The current nested ifs
around params.IsDraft and onlyMyResponse make intent unclear; simplify by
computing isDraft once: if onlyMyResponse then set isDraft = params.IsDraft
(allow nil) else if params.IsDraft == nil set a local submittedOnly := false and
set isDraft = &submittedOnly else set isDraft = params.IsDraft; then keep the
existing permission check that if isDraft != nil && *isDraft && !onlyMyResponse
returns 403; update references to isDraft in the call to q.GetRespondentDetails
and confirm that there is no administrator bypass required for viewing other
users' drafts (adjust tests or UI flow if that is a mismatch).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@controller/questionnaire.go`:
- Around line 1132-1143: The current nested ifs around params.IsDraft and
onlyMyResponse make intent unclear; simplify by computing isDraft once: if
onlyMyResponse then set isDraft = params.IsDraft (allow nil) else if
params.IsDraft == nil set a local submittedOnly := false and set isDraft =
&submittedOnly else set isDraft = params.IsDraft; then keep the existing
permission check that if isDraft != nil && *isDraft && !onlyMyResponse returns
403; update references to isDraft in the call to q.GetRespondentDetails and
confirm that there is no administrator bypass required for viewing other users'
drafts (adjust tests or UI flow if that is a mismatch).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 004d517b-1e0e-419d-9157-6735c3785f08

📥 Commits

Reviewing files that changed from the base of the PR and between 907a134 and 435e2d1.

📒 Files selected for processing (2)
  • controller/questionnaire.go
  • controller/questionnaire_test.go

@kaitoyama kaitoyama requested a review from Eraxyso April 21, 2026 14:37
Comment on lines +1132 to +1143
isDraft := params.IsDraft
if !onlyMyResponse {
submittedOnly := false
if params.IsDraft == nil {
isDraft = &submittedOnly
}
}
if isDraft != nil && *isDraft && !onlyMyResponse {
c.Logger().Infof("user %s is not allowed to view other respondents' drafts for questionnaire %d", userID, questionnaireID)
return res, echo.NewHTTPError(http.StatusForbidden, "you do not have permission to view other respondents' drafts")
}
respondentDetails, err := q.GetRespondentDetails(c.Request().Context(), questionnaireID, sort, onlyMyResponse, userID, isDraft)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
isDraft := params.IsDraft
if !onlyMyResponse {
submittedOnly := false
if params.IsDraft == nil {
isDraft = &submittedOnly
}
}
if isDraft != nil && *isDraft && !onlyMyResponse {
c.Logger().Infof("user %s is not allowed to view other respondents' drafts for questionnaire %d", userID, questionnaireID)
return res, echo.NewHTTPError(http.StatusForbidden, "you do not have permission to view other respondents' drafts")
}
respondentDetails, err := q.GetRespondentDetails(c.Request().Context(), questionnaireID, sort, onlyMyResponse, userID, isDraft)
if params.IsDraft != nil && *params.IsDraft {
onlyMyResponse = true
}
respondentDetails, err := q.GetRespondentDetails(c.Request().Context(), questionnaireID, sort, onlyMyResponse, userID, params.IsDraft)

ここはGET /questionnairesの実装に合わせて欲しいです

@kaitoyama kaitoyama requested a review from Eraxyso May 3, 2026 15:40
Copy link
Copy Markdown
Contributor

@Eraxyso Eraxyso left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with nits

MinLabel: &sampleQeustionsettingsScaleMinLabel,
MinValue: 1,
QuestionType: openapi.QuestionSettingsScaleQuestionTypeScale,
sampleQuestionnaireOnce.Do(func() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit]
deep copyになるので、sampleQuestionnaireはグローバル変数にする必要がなさそう
ローカル変数にすることで、sampleQuestionnaireOnceが不要になり、よりシンプルでクリーンな実装になると思います

@kaitoyama kaitoyama merged commit 2ccc37c into main May 5, 2026
15 checks passed
@kaitoyama kaitoyama deleted the codex/fix-draft-response-visibility branch May 5, 2026 01:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants