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")) } }