Skip to content

Commit 90d11bf

Browse files
buty4649claude
andcommitted
document: --me で /scim/v2/{domain_code}/Me をフォールバック
XPOINT_USER(もしくは --xpoint-user)が設定されていればそれを使い、未設定時は /scim/v2/{domain_code}/Me を呼んで認証ユーザの userName を取得するようにする。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 39a1798 commit 90d11bf

5 files changed

Lines changed: 141 additions & 57 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ xp document search --title 経費 # 件名部分一致
104104
xp document search --form-name 稟議 --form-group-id 3 # フォーム名 + フォームグループID
105105
xp document search --writer alice --writer bob # 申請者指定(複数可)
106106
xp document search --writer-group grp1 # 申請者グループ指定
107-
xp document search --me # 自分が申請者の書類
107+
xp document search --me # 自分が申請者の書類(XPOINT_USER、未設定なら /scim/v2/{domain_code}/Me から取得)
108108
xp document search --since 2024-01-01 --until 2024-12-31
109109
```
110110

cmd/document.go

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cmd
22

33
import (
4+
"context"
45
"encoding/json"
56
"fmt"
67
"io"
@@ -75,7 +76,8 @@ Filter flags build a search body automatically:
7576
--form-group-id <n> form group ID (fgid)
7677
--writer <code> writer user code (repeatable)
7778
--writer-group <code> writer user-group code (repeatable)
78-
--me shorthand for --writer <current user code>
79+
--me shorthand for --writer <current user code>;
80+
looked up via XPOINT_USER or /scim/v2/{domain_code}/Me
7981
--since <YYYY-MM-DD> lower bound of 新規更新日 (cr_dt)
8082
--until <YYYY-MM-DD> upper bound of 新規更新日 (cr_dt)
8183
@@ -173,7 +175,7 @@ func init() {
173175
f.IntVar(&docSearchFGID, "form-group-id", 0, "form group ID (fgid); 0 = omit")
174176
f.StringSliceVar(&docSearchWriters, "writer", nil, "writer user code (repeatable)")
175177
f.StringSliceVar(&docSearchGroups, "writer-group", nil, "writer user-group code (repeatable)")
176-
f.BoolVar(&docSearchMe, "me", false, "restrict to documents written by the current user (XPOINT_USER)")
178+
f.BoolVar(&docSearchMe, "me", false, "restrict to documents written by the current user (XPOINT_USER, or /scim/v2/{domain_code}/Me)")
177179
f.StringVar(&docSearchSince, "since", "", "lower bound of 新規更新日 (YYYY-MM-DD)")
178180
f.StringVar(&docSearchUntil, "until", "", "upper bound of 新規更新日 (YYYY-MM-DD)")
179181

@@ -218,7 +220,14 @@ func runDocumentSearch(cmd *cobra.Command, args []string) error {
218220
if len(bodyBytes) > 0 {
219221
return fmt.Errorf("--body cannot be combined with filter flags (--title, --form-*, --writer*, --me, --since, --until)")
220222
}
221-
built, err := buildSearchBodyFromFlags()
223+
meCode := ""
224+
if docSearchMe {
225+
meCode, err = resolveCurrentUserCode(cmd.Context(), client)
226+
if err != nil {
227+
return err
228+
}
229+
}
230+
built, err := buildSearchBodyFromFlags(meCode)
222231
if err != nil {
223232
return err
224233
}
@@ -460,9 +469,33 @@ type writerListEntry struct {
460469
Code string `json:"code"`
461470
}
462471

472+
// resolveCurrentUserCode returns the authenticated user's login ID for --me.
473+
// It prefers XPOINT_USER / --xpoint-user; if neither is set, it falls back to
474+
// GET /scim/v2/{domain_code}/Me (which requires OAuth2 and the domain code).
475+
func resolveCurrentUserCode(ctx context.Context, client *xpoint.Client) (string, error) {
476+
if u := pick(flagUser, "XPOINT_USER"); u != "" {
477+
return u, nil
478+
}
479+
domain := pick(flagDomainCode, "XPOINT_DOMAIN_CODE")
480+
if domain == "" {
481+
return "", fmt.Errorf("--me requires the current user code: set --xpoint-user / XPOINT_USER, or --xpoint-domain-code / XPOINT_DOMAIN_CODE to look it up via /scim/v2/{domain_code}/Me")
482+
}
483+
info, err := client.GetSelfInfo(ctx, domain)
484+
if err != nil {
485+
return "", fmt.Errorf("resolve --me via /scim/v2/%s/Me: %w", domain, err)
486+
}
487+
if info.UserName == "" {
488+
return "", fmt.Errorf("resolve --me: userName is empty in /scim/v2/%s/Me response", domain)
489+
}
490+
return info.UserName, nil
491+
}
492+
463493
// buildSearchBodyFromFlags converts --title / --form-* / --writer* / --me /
464494
// --since / --until into a JSON request body for POST /api/v1/search/documents.
465-
func buildSearchBodyFromFlags() (json.RawMessage, error) {
495+
//
496+
// meCode is the resolved user code to use for --me (empty if --me was not set
497+
// or resolution is not needed).
498+
func buildSearchBodyFromFlags(meCode string) (json.RawMessage, error) {
466499
body := map[string]any{}
467500

468501
if docSearchTitle != "" {
@@ -489,12 +522,8 @@ func buildSearchBodyFromFlags() (json.RawMessage, error) {
489522
writers = append(writers, writerListEntry{Type: "group", Code: code})
490523
}
491524
}
492-
if docSearchMe {
493-
me := pick(flagUser, "XPOINT_USER")
494-
if me == "" {
495-
return nil, fmt.Errorf("--me requires the current user code: set --xpoint-user or XPOINT_USER")
496-
}
497-
writers = append(writers, writerListEntry{Type: "user", Code: me})
525+
if meCode != "" {
526+
writers = append(writers, writerListEntry{Type: "user", Code: meCode})
498527
}
499528
if len(writers) > 0 {
500529
body["writer_list"] = writers

cmd/document_test.go

Lines changed: 50 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cmd
22

33
import (
4+
"context"
45
"encoding/json"
56
"io"
67
"os"
@@ -13,18 +14,7 @@ import (
1314
// zero values so each test starts from a clean slate.
1415
func resetSearchFlags(t *testing.T) {
1516
t.Helper()
16-
docSearchBody = ""
17-
docSearchTitle = ""
18-
docSearchFormName = ""
19-
docSearchFormID = 0
20-
docSearchFGID = 0
21-
docSearchWriters = nil
22-
docSearchGroups = nil
23-
docSearchMe = false
24-
docSearchSince = ""
25-
docSearchUntil = ""
26-
flagUser = ""
27-
t.Cleanup(func() {
17+
clear := func() {
2818
docSearchBody = ""
2919
docSearchTitle = ""
3020
docSearchFormName = ""
@@ -36,7 +26,10 @@ func resetSearchFlags(t *testing.T) {
3626
docSearchSince = ""
3727
docSearchUntil = ""
3828
flagUser = ""
39-
})
29+
flagDomainCode = ""
30+
}
31+
clear()
32+
t.Cleanup(clear)
4033
}
4134

4235
func TestBuildSearchBody_TitleAndForm(t *testing.T) {
@@ -46,7 +39,7 @@ func TestBuildSearchBody_TitleAndForm(t *testing.T) {
4639
docSearchFormID = 42
4740
docSearchFGID = 7
4841

49-
raw, err := buildSearchBodyFromFlags()
42+
raw, err := buildSearchBodyFromFlags("")
5043
if err != nil {
5144
t.Fatalf("build: %v", err)
5245
}
@@ -73,7 +66,7 @@ func TestBuildSearchBody_Writers(t *testing.T) {
7366
docSearchWriters = []string{"u1", "u2"}
7467
docSearchGroups = []string{"g1"}
7568

76-
raw, err := buildSearchBodyFromFlags()
69+
raw, err := buildSearchBodyFromFlags("")
7770
if err != nil {
7871
t.Fatalf("build: %v", err)
7972
}
@@ -100,41 +93,15 @@ func TestBuildSearchBody_Writers(t *testing.T) {
10093
}
10194
}
10295

103-
func TestBuildSearchBody_MeRequiresUser(t *testing.T) {
104-
resetSearchFlags(t)
105-
docSearchMe = true
106-
107-
_, err := buildSearchBodyFromFlags()
108-
if err == nil || !strings.Contains(err.Error(), "--me requires") {
109-
t.Errorf("err = %v", err)
110-
}
111-
}
112-
113-
func TestBuildSearchBody_MeWithFlagUser(t *testing.T) {
96+
func TestBuildSearchBody_MeCodeAppendsWriter(t *testing.T) {
11497
resetSearchFlags(t)
115-
docSearchMe = true
116-
flagUser = "alice"
11798

118-
raw, err := buildSearchBodyFromFlags()
99+
raw, err := buildSearchBodyFromFlags("alice")
119100
if err != nil {
120101
t.Fatalf("build: %v", err)
121102
}
122103
if !strings.Contains(string(raw), `"code":"alice"`) {
123-
t.Errorf("body missing current user: %s", string(raw))
124-
}
125-
}
126-
127-
func TestBuildSearchBody_MeFromEnv(t *testing.T) {
128-
resetSearchFlags(t)
129-
docSearchMe = true
130-
t.Setenv("XPOINT_USER", "bob")
131-
132-
raw, err := buildSearchBodyFromFlags()
133-
if err != nil {
134-
t.Fatalf("build: %v", err)
135-
}
136-
if !strings.Contains(string(raw), `"code":"bob"`) {
137-
t.Errorf("body missing env user: %s", string(raw))
104+
t.Errorf("body missing me user: %s", string(raw))
138105
}
139106
}
140107

@@ -143,7 +110,7 @@ func TestBuildSearchBody_SinceUntil(t *testing.T) {
143110
docSearchSince = "2024-01-15"
144111
docSearchUntil = "2024-12-31"
145112

146-
raw, err := buildSearchBodyFromFlags()
113+
raw, err := buildSearchBodyFromFlags("")
147114
if err != nil {
148115
t.Fatalf("build: %v", err)
149116
}
@@ -169,12 +136,49 @@ func TestBuildSearchBody_InvalidSince(t *testing.T) {
169136
resetSearchFlags(t)
170137
docSearchSince = "2024/01/15"
171138

172-
_, err := buildSearchBodyFromFlags()
139+
_, err := buildSearchBodyFromFlags("")
173140
if err == nil || !strings.Contains(err.Error(), "--since") {
174141
t.Errorf("err = %v", err)
175142
}
176143
}
177144

145+
func TestResolveCurrentUserCode_UsesFlagUser(t *testing.T) {
146+
resetSearchFlags(t)
147+
flagUser = "alice"
148+
149+
code, err := resolveCurrentUserCode(context.Background(), nil)
150+
if err != nil {
151+
t.Fatalf("resolve: %v", err)
152+
}
153+
if code != "alice" {
154+
t.Errorf("code = %q, want alice", code)
155+
}
156+
}
157+
158+
func TestResolveCurrentUserCode_UsesEnvUser(t *testing.T) {
159+
resetSearchFlags(t)
160+
t.Setenv("XPOINT_USER", "bob")
161+
162+
code, err := resolveCurrentUserCode(context.Background(), nil)
163+
if err != nil {
164+
t.Fatalf("resolve: %v", err)
165+
}
166+
if code != "bob" {
167+
t.Errorf("code = %q, want bob", code)
168+
}
169+
}
170+
171+
func TestResolveCurrentUserCode_ErrorsWithoutUserOrDomain(t *testing.T) {
172+
resetSearchFlags(t)
173+
t.Setenv("XPOINT_USER", "")
174+
t.Setenv("XPOINT_DOMAIN_CODE", "")
175+
176+
_, err := resolveCurrentUserCode(context.Background(), nil)
177+
if err == nil || !strings.Contains(err.Error(), "--xpoint-user") {
178+
t.Errorf("err = %v", err)
179+
}
180+
}
181+
178182
func TestRunDocumentSearch_BodyAndFilterConflict(t *testing.T) {
179183
resetSearchFlags(t)
180184
t.Setenv("XPOINT_SUBDOMAIN", "acme")

internal/xpoint/client.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,26 @@ func (c *Client) DeleteDocument(ctx context.Context, docID int) (*DeleteDocument
355355
return &out, nil
356356
}
357357

358+
type SelfInfo struct {
359+
ID string `json:"id"`
360+
UserName string `json:"userName"`
361+
DisplayName string `json:"displayName"`
362+
}
363+
364+
// GetSelfInfo calls GET /scim/v2/{domain_code}/Me to fetch the authenticated
365+
// user's info. Requires OAuth2 bearer auth.
366+
func (c *Client) GetSelfInfo(ctx context.Context, domainCode string) (*SelfInfo, error) {
367+
if domainCode == "" {
368+
return nil, fmt.Errorf("domain code is required for /scim/v2/{domain_code}/Me")
369+
}
370+
path := fmt.Sprintf("/scim/v2/%s/Me", url.PathEscape(domainCode))
371+
var out SelfInfo
372+
if err := c.do(ctx, http.MethodGet, path, nil, nil, &out); err != nil {
373+
return nil, err
374+
}
375+
return &out, nil
376+
}
377+
358378
// DownloadPDF calls GET /api/v1/documents/{docid}/pdf and returns the PDF
359379
// bytes and the server-provided filename (parsed from Content-Disposition,
360380
// which may use RFC 5987 encoding). The filename is empty when the server

internal/xpoint/client_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,37 @@ func TestDocumentURL(t *testing.T) {
356356
}
357357
}
358358

359+
func TestGetSelfInfo(t *testing.T) {
360+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
361+
if r.Method != http.MethodGet {
362+
t.Errorf("method = %s, want GET", r.Method)
363+
}
364+
if r.URL.Path != "/scim/v2/acme/Me" {
365+
t.Errorf("path = %s", r.URL.Path)
366+
}
367+
w.Header().Set("Content-Type", "application/scim+json")
368+
_, _ = w.Write([]byte(`{"id":"100","userName":"u001","displayName":"田中"}`))
369+
}))
370+
defer srv.Close()
371+
372+
c := clientForServer(srv)
373+
info, err := c.GetSelfInfo(context.Background(), "acme")
374+
if err != nil {
375+
t.Fatalf("GetSelfInfo: %v", err)
376+
}
377+
if info.UserName != "u001" || info.ID != "100" || info.DisplayName != "田中" {
378+
t.Errorf("info = %+v", info)
379+
}
380+
}
381+
382+
func TestGetSelfInfo_RequiresDomainCode(t *testing.T) {
383+
c := NewClient("unused", Auth{AccessToken: "t"})
384+
_, err := c.GetSelfInfo(context.Background(), "")
385+
if err == nil || !strings.Contains(err.Error(), "domain code is required") {
386+
t.Errorf("err = %v", err)
387+
}
388+
}
389+
359390
func TestDownloadPDF(t *testing.T) {
360391
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
361392
if r.Method != http.MethodGet {

0 commit comments

Comments
 (0)