Files
im_manager/server/internal/library/imadmin/client_test.go
T

143 lines
5.2 KiB
Go

package imadmin
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"time"
)
type testInviteListRequest struct {
Page int `json:"page,omitempty"`
PerPage int `json:"per_page,omitempty"`
InviteCode string `json:"invite_code,omitempty"`
BatchID int64 `json:"batch_id,omitempty"`
}
func TestListCodesUsesSignedGetWithSnakeQuery(t *testing.T) {
server := signedTestServer(t, http.MethodGet, "/admin/v1/im/registration-invite-codes", func(t *testing.T, r *http.Request, body []byte) {
if len(body) != 0 {
t.Fatalf("GET list should not send body, got %q", string(body))
}
query := r.URL.Query()
if query.Get("page") != "2" || query.Get("per_page") != "20" || query.Get("batch_id") != "123" {
t.Fatalf("unexpected query: %s", r.URL.RawQuery)
}
})
defer server.Close()
client := NewClient(Config{BaseUrl: server.URL, ServiceId: "hotgo-admin", ServiceSecret: "secret", Timeout: time.Second})
var out Page[struct{}]
err := client.ListCodes(context.Background(), AdminScope{TenantId: 9, OperatorId: 7, RequestId: "req-1"}, testInviteListRequest{Page: 2, PerPage: 20, BatchID: 123}, &out)
if err != nil {
t.Fatalf("ListCodes returned error: %v", err)
}
}
func TestListCodesWithInviteCodeUsesSignedSearchPost(t *testing.T) {
server := signedTestServer(t, http.MethodPost, "/admin/v1/im/registration-invite-codes/search", func(t *testing.T, r *http.Request, body []byte) {
if !strings.Contains(string(body), `"invite_code":"Ab123"`) {
t.Fatalf("POST search should send snake_case invite code, got %s", string(body))
}
})
defer server.Close()
client := NewClient(Config{BaseUrl: server.URL, ServiceId: "hotgo-admin", ServiceSecret: "secret", Timeout: time.Second})
var out Page[struct{}]
err := client.ListCodes(context.Background(), AdminScope{TenantId: 9, OperatorId: 7, RequestId: "req-2"}, testInviteListRequest{Page: 1, PerPage: 20, InviteCode: "Ab123"}, &out)
if err != nil {
t.Fatalf("ListCodes returned error: %v", err)
}
}
func TestInternalPostUsesInternalSignatureAndBody(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/internal/admin-query/tenants/default/users" {
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
}
body, err := io.ReadAll(r.Body)
if err != nil {
t.Fatalf("read body: %v", err)
}
if string(body) != `{"username":"alice"}` {
t.Fatalf("unexpected body %s", body)
}
if r.Header.Get("X-Service-Name") != "hotgo-admin" || r.Header.Get("X-Signature") == "" {
t.Fatalf("missing internal authentication headers")
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"code":0,"data":{"user_id":123,"username":"alice"}}`))
}))
defer server.Close()
client := NewClient(Config{InternalBaseUrl: server.URL, InternalServiceName: "hotgo-admin", InternalServiceSecret: "secret", Timeout: time.Second})
var out struct {
UserID int64 `json:"user_id"`
Username string `json:"username"`
}
if err := client.InternalPost(context.Background(), "/internal/admin-query/tenants/default/users", map[string]string{"username": "alice"}, &out); err != nil {
t.Fatalf("InternalPost returned error: %v", err)
}
if out.UserID != 123 || out.Username != "alice" {
t.Fatalf("unexpected response: %+v", out)
}
}
func signedTestServer(t *testing.T, method string, path string, inspect func(t *testing.T, r *http.Request, body []byte)) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != method {
t.Fatalf("method = %s, want %s", r.Method, method)
}
if r.URL.Path != path {
t.Fatalf("path = %s, want %s", r.URL.Path, path)
}
body, err := io.ReadAll(r.Body)
if err != nil {
t.Fatalf("read body: %v", err)
}
inspect(t, r, body)
assertSignedRequest(t, r, body, "secret")
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"data":{"items":[],"page":1,"per_page":20,"total":0}}`))
}))
}
func assertSignedRequest(t *testing.T, r *http.Request, body []byte, secret string) {
t.Helper()
for _, name := range []string{"Authorization", "X-IM-Service-ID", "X-IM-Tenant-ID", "X-IM-Operator-ID", "X-IM-Timestamp", "X-IM-Nonce", "X-Request-ID"} {
if r.Header.Get(name) == "" {
t.Fatalf("missing header %s", name)
}
}
if _, err := strconv.ParseInt(r.Header.Get("X-IM-Timestamp"), 10, 64); err != nil {
t.Fatalf("timestamp is not unix seconds: %v", err)
}
bodyHash := sha256.Sum256(body)
canonical := strings.Join([]string{
r.Method,
r.URL.RequestURI(),
r.Header.Get("X-IM-Service-ID"),
r.Header.Get("X-IM-Tenant-ID"),
r.Header.Get("X-IM-Operator-ID"),
r.Header.Get("X-IM-Timestamp"),
r.Header.Get("X-IM-Nonce"),
r.Header.Get("X-Request-ID"),
hex.EncodeToString(bodyHash[:]),
}, "\n")
mac := hmac.New(sha256.New, []byte(secret))
_, _ = mac.Write([]byte(canonical))
expected := fmt.Sprintf("IM-HMAC %s", hex.EncodeToString(mac.Sum(nil)))
if r.Header.Get("Authorization") != expected {
t.Fatalf("bad signature\nwant: %s\n got: %s", expected, r.Header.Get("Authorization"))
}
}