Repository: line/line-bot-sdk-go
Pull request #338 (2023-01-31 14:23:25)

feat(method): verify access token method
adding new method based on issue here https://github.com/line/line-bot-sdk-go/issues/337. please help to review @kkdai 

CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/line/line-bot-sdk-go?pullRequest=338) <br/>Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla-assistant.io/line/line-bot-sdk-go?pullRequest=338) before we can accept your contribution.<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/line/line-bot-sdk-go?pullRequest=338) it.</sub>
blackhun11: cc @kkdai 
codecov-commenter: # [Codecov](https://codecov.io/gh/line/line-bot-sdk-go/pull/338?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=line) Report
Base: **78.80**% // Head: **78.77**% // Decreases project coverage by **`-0.03%`** :warning:
> Coverage data is based on head [(`0b0a819`)](https://codecov.io/gh/line/line-bot-sdk-go/pull/338?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=line) compared to base [(`e049ef8`)](https://codecov.io/gh/line/line-bot-sdk-go/commit/e049ef8eb66bd589011eedeb3cf6a50bdf19796e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=line).
> Patch coverage: 75.00% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #338      +/-   ##
==========================================
- Coverage   78.80%   78.77%   -0.03%     
==========================================
  Files          39       39              
  Lines        3576     3600      +24     
==========================================
+ Hits         2818     2836      +18     
- Misses        578      581       +3     
- Partials      180      183       +3     
```


| [Impacted Files](https://codecov.io/gh/line/line-bot-sdk-go/pull/338?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=line) | Coverage Δ | |
|---|---|---|
| [linebot/client.go](https://codecov.io/gh/line/line-bot-sdk-go/pull/338?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=line#diff-bGluZWJvdC9jbGllbnQuZ28=) | `69.46% <ø> (ø)` | |
| [linebot/response.go](https://codecov.io/gh/line/line-bot-sdk-go/pull/338?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=line#diff-bGluZWJvdC9yZXNwb25zZS5nbw==) | `68.50% <50.00%> (-0.47%)` | :arrow_down: |
| [linebot/oauth.go](https://codecov.io/gh/line/line-bot-sdk-go/pull/338?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=line#diff-bGluZWJvdC9vYXV0aC5nbw==) | `88.23% <87.50%> (-0.34%)` | :arrow_down: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=line). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=line)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/line/line-bot-sdk-go/pull/338?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=line).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=line).

blackhun11: hi @kkdai , just in case you forget this. is it good to go?

Patch:
diff --git a/linebot/client.go b/linebot/client.go
index 7526baf..cb12fba 100644
--- a/linebot/client.go
+++ b/linebot/client.go
@@ -94,6 +94,7 @@ const (
 
 	APIEndpointIssueAccessToken  = "/v2/oauth/accessToken"
 	APIEndpointRevokeAccessToken = "/v2/oauth/revoke"
+	APIEndpointVerifyAccessToken = "/v2/oauth/verify"
 
 	APIEndpointIssueAccessTokenV2  = "/oauth2/v2.1/token"
 	APIEndpointGetAccessTokensV2   = "/oauth2/v2.1/tokens/kid"
diff --git a/linebot/oauth.go b/linebot/oauth.go
index cc373da..541d467 100644
--- a/linebot/oauth.go
+++ b/linebot/oauth.go
@@ -95,3 +95,39 @@ func (call *RevokeAccessTokenCall) Do() (*BasicResponse, error) {
 	defer closeResponse(res)
 	return decodeToBasicResponse(res)
 }
+
+// VerifyAccessToken method
+func (client *Client) VerifyAccessToken(accessToken string) *VerifyAccessTokenCall {
+	return &VerifyAccessTokenCall{
+		c:           client,
+		accessToken: accessToken,
+	}
+}
+
+// VerifyAccessTokenCall type
+type VerifyAccessTokenCall struct {
+	c   *Client
+	ctx context.Context
+
+	accessToken string
+}
+
+// WithContext method
+func (call *VerifyAccessTokenCall) WithContext(ctx context.Context) *VerifyAccessTokenCall {
+	call.ctx = ctx
+	return call
+}
+
+// Do method
+func (call *VerifyAccessTokenCall) Do() (*VerifiedAccessTokenResponse, error) {
+	vs := url.Values{}
+	vs.Set("access_token", call.accessToken)
+	body := strings.NewReader(vs.Encode())
+
+	res, err := call.c.postForm(call.ctx, APIEndpointVerifyAccessToken, body)
+	if err != nil {
+		return nil, err
+	}
+	defer closeResponse(res)
+	return decodeToVerifiedAccessTokenResponse(res)
+}
diff --git a/linebot/response.go b/linebot/response.go
index efb9f13..a62da93 100644
--- a/linebot/response.go
+++ b/linebot/response.go
@@ -400,6 +400,13 @@ type AccessTokensResponse struct {
 	KeyIDs []string `json:"kids"`
 }
 
+// VerifiedAccessTokenResponse type
+type VerifiedAccessTokenResponse struct {
+	ClientID  string `json:"client_id"`
+	ExpiresIn int64  `json:"expires_in"`
+	Scope     string `json:"scope"`
+}
+
 // TestWebhookResponse type
 type TestWebhookResponse struct {
 	Success    bool      `json:"success"`
@@ -763,6 +770,18 @@ func decodeToAccessTokensResponse(res *http.Response) (*AccessTokensResponse, er
 	return &result, nil
 }
 
+func decodeToVerifiedAccessTokenResponse(res *http.Response) (*VerifiedAccessTokenResponse, error) {
+	if err := checkResponse(res); err != nil {
+		return nil, err
+	}
+	decoder := json.NewDecoder(res.Body)
+	result := VerifiedAccessTokenResponse{}
+	if err := decoder.Decode(&result); err != nil {
+		return nil, err
+	}
+	return &result, nil
+}
+
 func decodeToTestWebhookResponse(res *http.Response) (*TestWebhookResponse, error) {
 	if err := checkResponse(res); err != nil {
 		return nil, err


Test patch:
diff --git a/linebot/oauth_test.go b/linebot/oauth_test.go
index 985d1ff..4c9085e 100644
--- a/linebot/oauth_test.go
+++ b/linebot/oauth_test.go
@@ -181,7 +181,7 @@ func TestRevokeAccessToken(t *testing.T) {
 			t.Errorf("Method %s; want %s", r.Method, http.MethodPost)
 		}
 		if r.URL.Path != APIEndpointRevokeAccessToken {
-			t.Errorf("URLPath %s; want %s", r.URL.Path, APIEndpointIssueAccessToken)
+			t.Errorf("URLPath %s; want %s", r.URL.Path, APIEndpointRevokeAccessToken)
 		}
 		body, err := ioutil.ReadAll(r.Body)
 		if err != nil {
@@ -274,3 +274,127 @@ func TestRevokeAccessTokenCall_WithContext(t *testing.T) {
 		})
 	}
 }
+
+func TestVerifyAccessToken(t *testing.T) {
+	type want struct {
+		RequestBody []byte
+		Response    *VerifiedAccessTokenResponse
+		Error       error
+	}
+	testCases := []struct {
+		AccessToken  string
+		Response     []byte
+		ResponseCode int
+		Want         want
+	}{
+		{
+			AccessToken:  "testtoken",
+			ResponseCode: 200,
+			Response:     []byte(`{}`),
+			Want: want{
+				RequestBody: []byte("access_token=testtoken"),
+				Response:    &VerifiedAccessTokenResponse{},
+			},
+		},
+	}
+
+	var currentTestIdx int
+	server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		defer r.Body.Close()
+		if r.Method != http.MethodPost {
+			t.Errorf("Method %s; want %s", r.Method, http.MethodPost)
+		}
+		if r.URL.Path != APIEndpointVerifyAccessToken {
+			t.Errorf("URLPath %s; want %s", r.URL.Path, APIEndpointVerifyAccessToken)
+		}
+		body, err := ioutil.ReadAll(r.Body)
+		if err != nil {
+			t.Fatal(err)
+		}
+		tc := testCases[currentTestIdx]
+		if !reflect.DeepEqual(body, tc.Want.RequestBody) {
+			t.Errorf("RequestBody %s; want %s", body, tc.Want.RequestBody)
+		}
+		w.WriteHeader(tc.ResponseCode)
+		w.Write(tc.Response)
+	}))
+	defer server.Close()
+
+	dataServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		defer r.Body.Close()
+		t.Error("Unexpected data API call")
+		w.WriteHeader(404)
+		w.Write([]byte(`{"message":"Not found"}`))
+	}))
+	defer dataServer.Close()
+
+	client, err := mockClient(server, dataServer)
+	if err != nil {
+		t.Fatal(err)
+	}
+	for i, tc := range testCases {
+		currentTestIdx = i
+		res, err := client.VerifyAccessToken(tc.AccessToken).Do()
+		if tc.Want.Error != nil {
+			if !reflect.DeepEqual(err, tc.Want.Error) {
+				t.Errorf("Error %d %q; want %q", i, err, tc.Want.Error)
+			}
+		} else {
+			if err != nil {
+				t.Error(err)
+			}
+		}
+		if tc.Want.Response != nil {
+			if !reflect.DeepEqual(res, tc.Want.Response) {
+				t.Errorf("Response %d %q; want %q", i, res, tc.Want.Response)
+			}
+		}
+	}
+}
+
+func TestVerifyAccessTokenCall_WithContext(t *testing.T) {
+	type fields struct {
+		c           *Client
+		ctx         context.Context
+		accessToken string
+	}
+	type args struct {
+		ctx context.Context
+	}
+
+	oldCtx := context.Background()
+	type key string
+	newCtx := context.WithValue(oldCtx, key("foo"), "bar")
+
+	tests := []struct {
+		name   string
+		fields fields
+		args   args
+		want   context.Context
+	}{
+		{
+			name: "replace context",
+			fields: fields{
+				ctx: oldCtx,
+			},
+			args: args{
+				ctx: newCtx,
+			},
+			want: newCtx,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			call := &VerifyAccessTokenCall{
+				c:           tt.fields.c,
+				ctx:         tt.fields.ctx,
+				accessToken: tt.fields.accessToken,
+			}
+			call = call.WithContext(tt.args.ctx)
+			got := call.ctx
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("VerifyAccessTokenCall.WithContext() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}