578 lines
19 KiB
Go
578 lines
19 KiB
Go
package questiontable
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestNewClientRejectsMissingConfig(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
cfg Config
|
|
}{
|
|
{"empty client id", Config{Secret: "s", BaseID: "b", SheetID: "sh", OperatorID: "op"}},
|
|
{"empty secret", Config{ClientID: "c", BaseID: "b", SheetID: "sh", OperatorID: "op"}},
|
|
{"empty base id", Config{ClientID: "c", Secret: "s", SheetID: "sh", OperatorID: "op"}},
|
|
{"empty sheet id", Config{ClientID: "c", Secret: "s", BaseID: "b", OperatorID: "op"}},
|
|
{"empty operator id", Config{ClientID: "c", Secret: "s", BaseID: "b", SheetID: "sh"}},
|
|
}
|
|
for _, tt := range tests {
|
|
_, err := NewClient(tt.cfg, nil)
|
|
if err == nil {
|
|
t.Errorf("%s: expected error, got nil", tt.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInsertRecordMethodPathHeaderBody(t *testing.T) {
|
|
var (
|
|
gotMethod string
|
|
gotPath string
|
|
gotQuery string
|
|
gotToken string
|
|
gotBody string
|
|
tokenCalls int32
|
|
insertCalls int32
|
|
)
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/v1.0/oauth2/accessToken", func(w http.ResponseWriter, r *http.Request) {
|
|
atomic.AddInt32(&tokenCalls, 1)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"accessToken":"test-access-token","expireIn":7200}`))
|
|
})
|
|
mux.HandleFunc("/v1.0/notable/bases/b1/sheets/sh1/records", func(w http.ResponseWriter, r *http.Request) {
|
|
atomic.AddInt32(&insertCalls, 1)
|
|
gotMethod = r.Method
|
|
gotPath = r.URL.Path
|
|
gotQuery = r.URL.RawQuery
|
|
gotToken = r.Header.Get("x-acs-dingtalk-access-token")
|
|
|
|
var body map[string]interface{}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
t.Errorf("decode body: %v", err)
|
|
}
|
|
records, ok := body["records"].([]interface{})
|
|
if !ok || len(records) != 1 {
|
|
t.Errorf("records must contain exactly one record: %#v", body["records"])
|
|
} else {
|
|
record, ok := records[0].(map[string]interface{})
|
|
if !ok {
|
|
t.Errorf("record must be an object: %#v", records[0])
|
|
} else {
|
|
fields, ok := record["fields"].(map[string]interface{})
|
|
if !ok {
|
|
t.Errorf("record must wrap values in a fields object: %#v", record)
|
|
} else if len(record) != 1 {
|
|
t.Errorf("record must contain only fields, got keys: %#v", record)
|
|
} else if _, ok := fields[FieldOriginalQuestion]; !ok {
|
|
t.Errorf("nested fields missing original question field")
|
|
}
|
|
}
|
|
}
|
|
raw, _ := json.Marshal(body)
|
|
gotBody = string(raw)
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"value":[{"id":"rec-abc-123"}]}`))
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
client, err := NewClient(Config{
|
|
ClientID: "appKey-1",
|
|
Secret: "appSecret-1",
|
|
BaseID: "b1",
|
|
SheetID: "sh1",
|
|
OperatorID: "operator-1",
|
|
TokenURL: server.URL + "/v1.0/oauth2/accessToken",
|
|
BaseURL: server.URL,
|
|
}, nil)
|
|
if err != nil {
|
|
t.Fatalf("NewClient: %v", err)
|
|
}
|
|
|
|
row := Row{
|
|
Question: "测试问题",
|
|
Answer: "测试答案",
|
|
AskerName: "张三",
|
|
AskerUnionID: "union-zhang-san",
|
|
}
|
|
recID, err := client.InsertRecord(context.Background(), row)
|
|
if err != nil {
|
|
t.Fatalf("InsertRecord: %v", err)
|
|
}
|
|
if recID != "rec-abc-123" {
|
|
t.Errorf("recordID = %q, want %q", recID, "rec-abc-123")
|
|
}
|
|
|
|
// Assert method, path, header, token.
|
|
if gotMethod != http.MethodPost {
|
|
t.Errorf("method = %q, want POST", gotMethod)
|
|
}
|
|
if gotPath != "/v1.0/notable/bases/b1/sheets/sh1/records" {
|
|
t.Errorf("path = %q", gotPath)
|
|
}
|
|
if gotToken != "test-access-token" {
|
|
t.Errorf("token header = %q, want test-access-token", gotToken)
|
|
}
|
|
|
|
// operatorId must be in query only.
|
|
if !strings.Contains(gotQuery, "operatorId=operator-1") {
|
|
t.Errorf("query missing operatorId: %q", gotQuery)
|
|
}
|
|
|
|
// body must NOT contain operatorId.
|
|
if strings.Contains(gotBody, "operatorId") {
|
|
t.Errorf("body contains operatorId: %q", gotBody)
|
|
}
|
|
|
|
// body must contain typed field IDs.
|
|
if !strings.Contains(gotBody, `"ZaMZF6j"`) {
|
|
t.Errorf("body missing field ID ZaMZF6j: %q", gotBody)
|
|
}
|
|
if !strings.Contains(gotBody, `"Y4cpmxU"`) {
|
|
t.Errorf("body missing field ID Y4cpmxU: %q", gotBody)
|
|
}
|
|
|
|
// body must NOT contain the omitted select fields.
|
|
omitted := []string{"QMn9UoU", "H3gtTuv", "nFvv8OR", "UMqUPAG", "lnF7gF9"}
|
|
for _, fid := range omitted {
|
|
if strings.Contains(gotBody, fid) {
|
|
t.Errorf("body contains omitted select field %q: %q", fid, gotBody)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTokenCacheTwoInsertsOneTokenCall(t *testing.T) {
|
|
var tokenCalls int32
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/v1.0/oauth2/accessToken", func(w http.ResponseWriter, r *http.Request) {
|
|
atomic.AddInt32(&tokenCalls, 1)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"accessToken":"cached-token","expireIn":7200}`))
|
|
})
|
|
mux.HandleFunc("/v1.0/notable/bases/b1/sheets/sh1/records", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get("x-acs-dingtalk-access-token") != "cached-token" {
|
|
t.Errorf("unexpected token on insert")
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"value":[{"id":"rec-1"}]}`))
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
client, err := NewClient(Config{
|
|
ClientID: "appKey-1",
|
|
Secret: "appSecret-1",
|
|
BaseID: "b1",
|
|
SheetID: "sh1",
|
|
OperatorID: "operator-1",
|
|
TokenURL: server.URL + "/v1.0/oauth2/accessToken",
|
|
BaseURL: server.URL,
|
|
}, nil)
|
|
if err != nil {
|
|
t.Fatalf("NewClient: %v", err)
|
|
}
|
|
|
|
row := Row{Question: "q", Answer: "a", AskerName: "x", AskerUnionID: "union-x"}
|
|
|
|
// Two inserts, only one token call.
|
|
for i := 0; i < 2; i++ {
|
|
if _, err := client.InsertRecord(context.Background(), row); err != nil {
|
|
t.Fatalf("InsertRecord %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
if tokenCalls != 1 {
|
|
t.Errorf("token calls = %d, want 1", tokenCalls)
|
|
}
|
|
}
|
|
|
|
func TestInsertRecordNon2xx(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/v1.0/oauth2/accessToken", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"accessToken":"tok","expireIn":7200}`))
|
|
})
|
|
mux.HandleFunc("/v1.0/notable/bases/b1/sheets/sh1/records", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
_, _ = w.Write([]byte(`{"code":"invalid","message":"bad request"}`))
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
client, _ := NewClient(Config{
|
|
ClientID: "c", Secret: "s", BaseID: "b1", SheetID: "sh1", OperatorID: "op",
|
|
TokenURL: server.URL + "/v1.0/oauth2/accessToken", BaseURL: server.URL,
|
|
}, nil)
|
|
|
|
row := Row{Question: "q", Answer: "a", AskerName: "x", AskerUnionID: "union-x"}
|
|
_, err := client.InsertRecord(context.Background(), row)
|
|
if err == nil {
|
|
t.Fatal("expected error for non-2xx")
|
|
}
|
|
|
|
// Error must not contain token, secret, operator, or raw row values.
|
|
errStr := err.Error()
|
|
for _, secret := range []string{"tok", "op", "union-x"} {
|
|
if strings.Contains(errStr, secret) {
|
|
t.Errorf("error redaction failed: %q found in %q", secret, errStr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInsertRecordMalformedResponse(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/v1.0/oauth2/accessToken", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"accessToken":"tok","expireIn":7200}`))
|
|
})
|
|
mux.HandleFunc("/v1.0/notable/bases/b1/sheets/sh1/records", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`not json`))
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
client, _ := NewClient(Config{
|
|
ClientID: "c", Secret: "s", BaseID: "b1", SheetID: "sh1", OperatorID: "op",
|
|
TokenURL: server.URL + "/v1.0/oauth2/accessToken", BaseURL: server.URL,
|
|
}, nil)
|
|
|
|
row := Row{Question: "q", Answer: "a", AskerName: "x", AskerUnionID: "union-x"}
|
|
_, err := client.InsertRecord(context.Background(), row)
|
|
if err == nil {
|
|
t.Fatal("expected error for malformed response")
|
|
}
|
|
}
|
|
|
|
func TestInsertRecordEmptyValueList(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/v1.0/oauth2/accessToken", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"accessToken":"tok","expireIn":7200}`))
|
|
})
|
|
mux.HandleFunc("/v1.0/notable/bases/b1/sheets/sh1/records", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"value":[]}`))
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
client, _ := NewClient(Config{
|
|
ClientID: "c", Secret: "s", BaseID: "b1", SheetID: "sh1", OperatorID: "op",
|
|
TokenURL: server.URL + "/v1.0/oauth2/accessToken", BaseURL: server.URL,
|
|
}, nil)
|
|
|
|
row := Row{Question: "q", Answer: "a", AskerName: "x", AskerUnionID: "union-x"}
|
|
_, err := client.InsertRecord(context.Background(), row)
|
|
if err == nil {
|
|
t.Fatal("expected error for empty value list")
|
|
}
|
|
}
|
|
|
|
func TestInsertRecordEmptyRecordID(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/v1.0/oauth2/accessToken", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"accessToken":"tok","expireIn":7200}`))
|
|
})
|
|
mux.HandleFunc("/v1.0/notable/bases/b1/sheets/sh1/records", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"value":[{"id":""}]}`))
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
client, _ := NewClient(Config{
|
|
ClientID: "c", Secret: "s", BaseID: "b1", SheetID: "sh1", OperatorID: "op",
|
|
TokenURL: server.URL + "/v1.0/oauth2/accessToken", BaseURL: server.URL,
|
|
}, nil)
|
|
|
|
row := Row{Question: "q", Answer: "a", AskerName: "x", AskerUnionID: "union-x"}
|
|
_, err := client.InsertRecord(context.Background(), row)
|
|
if err == nil {
|
|
t.Fatal("expected error for empty record id")
|
|
}
|
|
}
|
|
|
|
func TestTokenCallErrorRedaction(t *testing.T) {
|
|
// Token endpoint returns non-2xx.
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/v1.0/oauth2/accessToken", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
_, _ = w.Write([]byte(`{"code":"unauthorized","message":"invalid appSecret"}`))
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
client, _ := NewClient(Config{
|
|
ClientID: "c", Secret: "s", BaseID: "b1", SheetID: "sh1", OperatorID: "op",
|
|
TokenURL: server.URL + "/v1.0/oauth2/accessToken", BaseURL: server.URL,
|
|
}, nil)
|
|
|
|
row := Row{Question: "q", Answer: "a", AskerName: "x", AskerUnionID: "union-x"}
|
|
_, err := client.InsertRecord(context.Background(), row)
|
|
if err == nil {
|
|
t.Fatal("expected error for token failure")
|
|
}
|
|
|
|
// Error must not contain raw body, secret, or token.
|
|
errStr := err.Error()
|
|
for _, secret := range []string{"appSecret", "invalid appSecret", "unauthorized"} {
|
|
if strings.Contains(errStr, secret) {
|
|
t.Errorf("error redaction failed: %q found in %q", secret, errStr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTokenEmptyAccessToken(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/v1.0/oauth2/accessToken", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"accessToken":"","expireIn":7200}`))
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
client, _ := NewClient(Config{
|
|
ClientID: "c", Secret: "s", BaseID: "b1", SheetID: "sh1", OperatorID: "op",
|
|
TokenURL: server.URL + "/v1.0/oauth2/accessToken", BaseURL: server.URL,
|
|
}, nil)
|
|
|
|
row := Row{Question: "q", Answer: "a", AskerName: "x", AskerUnionID: "union-x"}
|
|
_, err := client.InsertRecord(context.Background(), row)
|
|
if err == nil {
|
|
t.Fatal("expected error for empty access token")
|
|
}
|
|
}
|
|
|
|
func TestTokenExpirySafetyMargin(t *testing.T) {
|
|
// Token with expireIn=1: TTL ≤ 30s → exp = TTL/2 (500ms). The tokenExp
|
|
// must be after now but no later than server expiry (1s) + small scheduling
|
|
// tolerance. An immediate second request should still reuse the cache.
|
|
var tokenCalls int32
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/v1.0/oauth2/accessToken", func(w http.ResponseWriter, r *http.Request) {
|
|
atomic.AddInt32(&tokenCalls, 1)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"accessToken":"short-token","expireIn":1}`))
|
|
})
|
|
mux.HandleFunc("/v1.0/notable/bases/b1/sheets/sh1/records", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"value":[{"id":"rec-1"}]}`))
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
client, _ := NewClient(Config{
|
|
ClientID: "c", Secret: "s", BaseID: "b1", SheetID: "sh1", OperatorID: "op",
|
|
TokenURL: server.URL + "/v1.0/oauth2/accessToken", BaseURL: server.URL,
|
|
}, nil)
|
|
|
|
row := Row{Question: "q", Answer: "a", AskerName: "x", AskerUnionID: "union-x"}
|
|
|
|
// First call: token fetch.
|
|
before := time.Now()
|
|
_, err := client.InsertRecord(context.Background(), row)
|
|
if err != nil {
|
|
t.Fatalf("first InsertRecord: %v", err)
|
|
}
|
|
if tokenCalls != 1 {
|
|
t.Errorf("token calls after first insert = %d, want 1", tokenCalls)
|
|
}
|
|
|
|
// Inspect tokenExp.
|
|
client.mu.Lock()
|
|
exp := client.tokenExp
|
|
client.mu.Unlock()
|
|
|
|
// tokenExp must be after now.
|
|
if !exp.After(before) {
|
|
t.Errorf("tokenExp %v is not after request start %v", exp, before)
|
|
}
|
|
// tokenExp must not exceed server expiry (1s) + scheduling tolerance (500ms).
|
|
if exp.After(before.Add(1500 * time.Millisecond)) {
|
|
t.Errorf("tokenExp %v exceeds server expiry + tolerance", exp)
|
|
}
|
|
|
|
// Second call: should use cached token (within TTL/2 = 500ms).
|
|
_, err = client.InsertRecord(context.Background(), row)
|
|
if err != nil {
|
|
t.Fatalf("second InsertRecord: %v", err)
|
|
}
|
|
if tokenCalls != 1 {
|
|
t.Errorf("token calls after second insert = %d, want 1 (cached)", tokenCalls)
|
|
}
|
|
}
|
|
|
|
func TestTokenExpiryNonPositive(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/v1.0/oauth2/accessToken", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"accessToken":"tok","expireIn":0}`))
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
client, _ := NewClient(Config{
|
|
ClientID: "c", Secret: "s", BaseID: "b1", SheetID: "sh1", OperatorID: "op",
|
|
TokenURL: server.URL + "/v1.0/oauth2/accessToken", BaseURL: server.URL,
|
|
}, nil)
|
|
|
|
row := Row{Question: "q", Answer: "a", AskerName: "x", AskerUnionID: "union-x"}
|
|
_, err := client.InsertRecord(context.Background(), row)
|
|
if err == nil {
|
|
t.Fatal("expected error for non-positive expireIn")
|
|
}
|
|
}
|
|
|
|
func TestContextTimeout(t *testing.T) {
|
|
handlerDone := make(chan struct{})
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/v1.0/oauth2/accessToken", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"accessToken":"tok","expireIn":7200}`))
|
|
})
|
|
mux.HandleFunc("/v1.0/notable/bases/b1/sheets/sh1/records", func(w http.ResponseWriter, r *http.Request) {
|
|
// Wait for context cancellation or test signal.
|
|
select {
|
|
case <-r.Context().Done():
|
|
case <-handlerDone:
|
|
}
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
defer close(handlerDone) // runs before server.Close (LIFO), unblocks the handler
|
|
|
|
// Use a client with keep-alives disabled so the TCP connection
|
|
// closes after the handler returns — httptest.Server.Close won't hang.
|
|
httpClient := &http.Client{
|
|
Transport: &http.Transport{DisableKeepAlives: true},
|
|
Timeout: 30 * time.Second,
|
|
}
|
|
client, _ := NewClient(Config{
|
|
ClientID: "c", Secret: "s", BaseID: "b1", SheetID: "sh1", OperatorID: "op",
|
|
TokenURL: server.URL + "/v1.0/oauth2/accessToken", BaseURL: server.URL,
|
|
}, httpClient)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
|
|
defer cancel()
|
|
|
|
row := Row{Question: "q", Answer: "a", AskerName: "x", AskerUnionID: "union-x"}
|
|
_, err := client.InsertRecord(ctx, row)
|
|
if err == nil {
|
|
t.Fatal("expected timeout error")
|
|
}
|
|
if !errors.Is(err, context.DeadlineExceeded) && !strings.Contains(err.Error(), "context deadline") {
|
|
t.Logf("timeout error (may be wrapped): %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDefaultURLs(t *testing.T) {
|
|
client, err := NewClient(Config{
|
|
ClientID: "c", Secret: "s", BaseID: "b1", SheetID: "sh1", OperatorID: "op",
|
|
}, nil)
|
|
if err != nil {
|
|
t.Fatalf("NewClient: %v", err)
|
|
}
|
|
if client.TokenURL() != defaultBase+"/v1.0/oauth2/accessToken" {
|
|
t.Errorf("default token URL = %q, want %q", client.TokenURL(), defaultBase+"/v1.0/oauth2/accessToken")
|
|
}
|
|
if client.BaseURL() != defaultBase {
|
|
t.Errorf("default base URL = %q, want %q", client.BaseURL(), defaultBase)
|
|
}
|
|
}
|
|
|
|
func TestCustomURLs(t *testing.T) {
|
|
client, err := NewClient(Config{
|
|
ClientID: "c", Secret: "s", BaseID: "b1", SheetID: "sh1", OperatorID: "op",
|
|
TokenURL: "https://custom.example.com/token",
|
|
BaseURL: "https://custom.example.com",
|
|
}, nil)
|
|
if err != nil {
|
|
t.Fatalf("NewClient: %v", err)
|
|
}
|
|
if client.TokenURL() != "https://custom.example.com/token" {
|
|
t.Errorf("custom token URL = %q", client.TokenURL())
|
|
}
|
|
if client.BaseURL() != "https://custom.example.com" {
|
|
t.Errorf("custom base URL = %q", client.BaseURL())
|
|
}
|
|
}
|
|
|
|
func TestNewClientNilHTTP(t *testing.T) {
|
|
client, err := NewClient(Config{
|
|
ClientID: "c", Secret: "s", BaseID: "b1", SheetID: "sh1", OperatorID: "op",
|
|
}, nil)
|
|
if err != nil {
|
|
t.Fatalf("NewClient with nil http: %v", err)
|
|
}
|
|
if client.http == nil {
|
|
t.Fatal("http client is nil after construction with nil argument")
|
|
}
|
|
}
|
|
|
|
func TestNewClientCustomHTTP(t *testing.T) {
|
|
custom := &http.Client{Timeout: 5 * time.Second}
|
|
client, err := NewClient(Config{
|
|
ClientID: "c", Secret: "s", BaseID: "b1", SheetID: "sh1", OperatorID: "op",
|
|
}, custom)
|
|
if err != nil {
|
|
t.Fatalf("NewClient with custom http: %v", err)
|
|
}
|
|
if client.http != custom {
|
|
t.Fatal("http client is not the custom one")
|
|
}
|
|
}
|
|
|
|
func TestDefaultTransportNoProxy(t *testing.T) {
|
|
// The default transport must have Proxy=nil.
|
|
if defaultTransport.Proxy != nil {
|
|
t.Error("defaultTransport Proxy is not nil")
|
|
}
|
|
}
|
|
|
|
func TestInsertRecordDoErrorRedaction(t *testing.T) {
|
|
// Custom RoundTripper that returns an error containing secrets.
|
|
secretErr := errors.New("connect: dial tcp https://api.dingtalk.com?operatorId=secret-op failed: row data union-x")
|
|
rt := &errorRoundTripper{err: secretErr}
|
|
|
|
client, _ := NewClient(Config{
|
|
ClientID: "c", Secret: "s", BaseID: "b1", SheetID: "sh1", OperatorID: "op",
|
|
TokenURL: "https://token.example.com", BaseURL: "https://api.example.com",
|
|
}, &http.Client{Transport: rt})
|
|
|
|
row := Row{Question: "q", Answer: "a", AskerName: "x", AskerUnionID: "union-x"}
|
|
_, err := client.InsertRecord(context.Background(), row)
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
|
|
errStr := err.Error()
|
|
for _, secret := range []string{"secret-op", "union-x", "api.dingtalk.com"} {
|
|
if strings.Contains(strings.ToLower(errStr), strings.ToLower(secret)) {
|
|
t.Errorf("error redaction failed: %q found in %q", secret, errStr)
|
|
}
|
|
}
|
|
}
|
|
|
|
// errorRoundTripper returns a fixed error on every RoundTrip.
|
|
type errorRoundTripper struct {
|
|
err error
|
|
}
|
|
|
|
func (rt *errorRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
return nil, rt.err
|
|
}
|