Repository: google/go-github (go)

Issue:
Move RateLimits method to a service
I missed the RateLimits method in #2937. It should be moved to a service struct.

Docs are at [rate-limit/rate-limit#get-rate-limit-status-for-the-authenticated-user](https://docs.github.com/rest/rate-limit/rate-limit#get-rate-limit-status-for-the-authenticated-user), so according to our existing pattern, we should move it to a new service called `RateLimitService` and name it `Get`

`Client.RateLimits` should be marked as deprecated and its body should be updated to `return c.RateLimit.Get(ctx)`

This may be a candidate for "good first issue" with the slight complication of creating a new service.

/cc #2936


Pull request:
Move RateLimits method to a service
Closes #2968 

Patch:
diff --git a/github/github.go b/github/github.go
index d71236ef..edd86ab2 100644
--- a/github/github.go
+++ b/github/github.go
@@ -203,6 +203,7 @@ type Client struct {
 	Organizations      *OrganizationsService
 	Projects           *ProjectsService
 	PullRequests       *PullRequestsService
+	RateLimit          *RateLimitService
 	Reactions          *ReactionsService
 	Repositories       *RepositoriesService
 	SCIM               *SCIMService
@@ -424,6 +425,7 @@ func (c *Client) initialize() {
 	c.Organizations = (*OrganizationsService)(&c.common)
 	c.Projects = (*ProjectsService)(&c.common)
 	c.PullRequests = (*PullRequestsService)(&c.common)
+	c.RateLimit = (*RateLimitService)(&c.common)
 	c.Reactions = (*ReactionsService)(&c.common)
 	c.Repositories = (*RepositoriesService)(&c.common)
 	c.SCIM = (*SCIMService)(&c.common)
@@ -1281,54 +1283,6 @@ func parseBoolResponse(err error) (bool, error) {
 	return false, err
 }
 
-// Rate represents the rate limit for the current client.
-type Rate struct {
-	// The number of requests per hour the client is currently limited to.
-	Limit int `json:"limit"`
-
-	// The number of remaining requests the client can make this hour.
-	Remaining int `json:"remaining"`
-
-	// The time at which the current rate limit will reset.
-	Reset Timestamp `json:"reset"`
-}
-
-func (r Rate) String() string {
-	return Stringify(r)
-}
-
-// RateLimits represents the rate limits for the current client.
-type RateLimits struct {
-	// The rate limit for non-search API requests. Unauthenticated
-	// requests are limited to 60 per hour. Authenticated requests are
-	// limited to 5,000 per hour.
-	//
-	// GitHub API docs: https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting
-	Core *Rate `json:"core"`
-
-	// The rate limit for search API requests. Unauthenticated requests
-	// are limited to 10 requests per minutes. Authenticated requests are
-	// limited to 30 per minute.
-	//
-	// GitHub API docs: https://docs.github.com/en/rest/search#rate-limit
-	Search *Rate `json:"search"`
-
-	// GitHub API docs: https://docs.github.com/en/graphql/overview/resource-limitations#rate-limit
-	GraphQL *Rate `json:"graphql"`
-
-	// GitHub API dos: https://docs.github.com/en/rest/rate-limit
-	IntegrationManifest *Rate `json:"integration_manifest"`
-
-	SourceImport              *Rate `json:"source_import"`
-	CodeScanningUpload        *Rate `json:"code_scanning_upload"`
-	ActionsRunnerRegistration *Rate `json:"actions_runner_registration"`
-	SCIM                      *Rate `json:"scim"`
-}
-
-func (r RateLimits) String() string {
-	return Stringify(r)
-}
-
 type rateLimitCategory uint8
 
 const (
@@ -1378,53 +1332,10 @@ func category(method, path string) rateLimitCategory {
 }
 
 // RateLimits returns the rate limits for the current client.
+//
+// Deprecated: Use RateLimitService.Get instead.
 func (c *Client) RateLimits(ctx context.Context) (*RateLimits, *Response, error) {
-	req, err := c.NewRequest("GET", "rate_limit", nil)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	response := new(struct {
-		Resources *RateLimits `json:"resources"`
-	})
-
-	// This resource is not subject to rate limits.
-	ctx = context.WithValue(ctx, bypassRateLimitCheck, true)
-	resp, err := c.Do(ctx, req, response)
-	if err != nil {
-		return nil, resp, err
-	}
-
-	if response.Resources != nil {
-		c.rateMu.Lock()
-		if response.Resources.Core != nil {
-			c.rateLimits[coreCategory] = *response.Resources.Core
-		}
-		if response.Resources.Search != nil {
-			c.rateLimits[searchCategory] = *response.Resources.Search
-		}
-		if response.Resources.GraphQL != nil {
-			c.rateLimits[graphqlCategory] = *response.Resources.GraphQL
-		}
-		if response.Resources.IntegrationManifest != nil {
-			c.rateLimits[integrationManifestCategory] = *response.Resources.IntegrationManifest
-		}
-		if response.Resources.SourceImport != nil {
-			c.rateLimits[sourceImportCategory] = *response.Resources.SourceImport
-		}
-		if response.Resources.CodeScanningUpload != nil {
-			c.rateLimits[codeScanningUploadCategory] = *response.Resources.CodeScanningUpload
-		}
-		if response.Resources.ActionsRunnerRegistration != nil {
-			c.rateLimits[actionsRunnerRegistrationCategory] = *response.Resources.ActionsRunnerRegistration
-		}
-		if response.Resources.SCIM != nil {
-			c.rateLimits[scimCategory] = *response.Resources.SCIM
-		}
-		c.rateMu.Unlock()
-	}
-
-	return response.Resources, resp, nil
+	return c.RateLimit.Get(ctx)
 }
 
 func setCredentialsAsHeaders(req *http.Request, id, secret string) *http.Request {
diff --git a/github/rate_limit.go b/github/rate_limit.go
new file mode 100644
index 00000000..4243a144
--- /dev/null
+++ b/github/rate_limit.go
@@ -0,0 +1,109 @@
+// Copyright 2023 The go-github AUTHORS. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package github
+
+import "context"
+
+// RateLimitService provides access to rate limit functions in the GitHub API.
+type RateLimitService service
+
+// Rate represents the rate limit for the current client.
+type Rate struct {
+	// The number of requests per hour the client is currently limited to.
+	Limit int `json:"limit"`
+
+	// The number of remaining requests the client can make this hour.
+	Remaining int `json:"remaining"`
+
+	// The time at which the current rate limit will reset.
+	Reset Timestamp `json:"reset"`
+}
+
+func (r Rate) String() string {
+	return Stringify(r)
+}
+
+// RateLimits represents the rate limits for the current client.
+type RateLimits struct {
+	// The rate limit for non-search API requests. Unauthenticated
+	// requests are limited to 60 per hour. Authenticated requests are
+	// limited to 5,000 per hour.
+	//
+	// GitHub API docs: https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting
+	Core *Rate `json:"core"`
+
+	// The rate limit for search API requests. Unauthenticated requests
+	// are limited to 10 requests per minutes. Authenticated requests are
+	// limited to 30 per minute.
+	//
+	// GitHub API docs: https://docs.github.com/en/rest/search#rate-limit
+	Search *Rate `json:"search"`
+
+	// GitHub API docs: https://docs.github.com/en/graphql/overview/resource-limitations#rate-limit
+	GraphQL *Rate `json:"graphql"`
+
+	// GitHub API dos: https://docs.github.com/en/rest/rate-limit
+	IntegrationManifest *Rate `json:"integration_manifest"`
+
+	SourceImport              *Rate `json:"source_import"`
+	CodeScanningUpload        *Rate `json:"code_scanning_upload"`
+	ActionsRunnerRegistration *Rate `json:"actions_runner_registration"`
+	SCIM                      *Rate `json:"scim"`
+}
+
+func (r RateLimits) String() string {
+	return Stringify(r)
+}
+
+// RateLimits returns the rate limits for the current client.
+func (s *RateLimitService) Get(ctx context.Context) (*RateLimits, *Response, error) {
+	req, err := s.client.NewRequest("GET", "rate_limit", nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	response := new(struct {
+		Resources *RateLimits `json:"resources"`
+	})
+
+	// This resource is not subject to rate limits.
+	ctx = context.WithValue(ctx, bypassRateLimitCheck, true)
+	resp, err := s.client.Do(ctx, req, response)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	if response.Resources != nil {
+		s.client.rateMu.Lock()
+		if response.Resources.Core != nil {
+			s.client.rateLimits[coreCategory] = *response.Resources.Core
+		}
+		if response.Resources.Search != nil {
+			s.client.rateLimits[searchCategory] = *response.Resources.Search
+		}
+		if response.Resources.GraphQL != nil {
+			s.client.rateLimits[graphqlCategory] = *response.Resources.GraphQL
+		}
+		if response.Resources.IntegrationManifest != nil {
+			s.client.rateLimits[integrationManifestCategory] = *response.Resources.IntegrationManifest
+		}
+		if response.Resources.SourceImport != nil {
+			s.client.rateLimits[sourceImportCategory] = *response.Resources.SourceImport
+		}
+		if response.Resources.CodeScanningUpload != nil {
+			s.client.rateLimits[codeScanningUploadCategory] = *response.Resources.CodeScanningUpload
+		}
+		if response.Resources.ActionsRunnerRegistration != nil {
+			s.client.rateLimits[actionsRunnerRegistrationCategory] = *response.Resources.ActionsRunnerRegistration
+		}
+		if response.Resources.SCIM != nil {
+			s.client.rateLimits[scimCategory] = *response.Resources.SCIM
+		}
+		s.client.rateMu.Unlock()
+	}
+
+	return response.Resources, resp, nil
+}


Test patch:
diff --git a/github/github_test.go b/github/github_test.go
index 7d92946d..90e95b03 100644
--- a/github/github_test.go
+++ b/github/github_test.go
@@ -468,23 +468,6 @@ func TestClient_rateLimits(t *testing.T) {
 	}
 }
 
-func TestRateLimits_String(t *testing.T) {
-	v := RateLimits{
-		Core:                      &Rate{},
-		Search:                    &Rate{},
-		GraphQL:                   &Rate{},
-		IntegrationManifest:       &Rate{},
-		SourceImport:              &Rate{},
-		CodeScanningUpload:        &Rate{},
-		ActionsRunnerRegistration: &Rate{},
-		SCIM:                      &Rate{},
-	}
-	want := `github.RateLimits{Core:github.Rate{Limit:0, Remaining:0, Reset:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}}, Search:github.Rate{Limit:0, Remaining:0, Reset:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}}, GraphQL:github.Rate{Limit:0, Remaining:0, Reset:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}}, IntegrationManifest:github.Rate{Limit:0, Remaining:0, Reset:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}}, SourceImport:github.Rate{Limit:0, Remaining:0, Reset:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}}, CodeScanningUpload:github.Rate{Limit:0, Remaining:0, Reset:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}}, ActionsRunnerRegistration:github.Rate{Limit:0, Remaining:0, Reset:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}}, SCIM:github.Rate{Limit:0, Remaining:0, Reset:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}}}`
-	if got := v.String(); got != want {
-		t.Errorf("RateLimits.String = %v, want %v", got, want)
-	}
-}
-
 func TestNewRequest(t *testing.T) {
 	c := NewClient(nil)
 
@@ -2123,251 +2106,6 @@ func TestError_Error(t *testing.T) {
 	}
 }
 
-func TestRateLimits(t *testing.T) {
-	client, mux, _, teardown := setup()
-	defer teardown()
-
-	mux.HandleFunc("/rate_limit", func(w http.ResponseWriter, r *http.Request) {
-		testMethod(t, r, "GET")
-		fmt.Fprint(w, `{"resources":{
-			"core": {"limit":2,"remaining":1,"reset":1372700873},
-			"search": {"limit":3,"remaining":2,"reset":1372700874},
-			"graphql": {"limit":4,"remaining":3,"reset":1372700875},
-			"integration_manifest": {"limit":5,"remaining":4,"reset":1372700876},
-			"source_import": {"limit":6,"remaining":5,"reset":1372700877},
-			"code_scanning_upload": {"limit":7,"remaining":6,"reset":1372700878},
-			"actions_runner_registration": {"limit":8,"remaining":7,"reset":1372700879},
-			"scim": {"limit":9,"remaining":8,"reset":1372700880}
-		}}`)
-	})
-
-	ctx := context.Background()
-	rate, _, err := client.RateLimits(ctx)
-	if err != nil {
-		t.Errorf("RateLimits returned error: %v", err)
-	}
-
-	want := &RateLimits{
-		Core: &Rate{
-			Limit:     2,
-			Remaining: 1,
-			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 53, 0, time.UTC).Local()},
-		},
-		Search: &Rate{
-			Limit:     3,
-			Remaining: 2,
-			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 54, 0, time.UTC).Local()},
-		},
-		GraphQL: &Rate{
-			Limit:     4,
-			Remaining: 3,
-			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 55, 0, time.UTC).Local()},
-		},
-		IntegrationManifest: &Rate{
-			Limit:     5,
-			Remaining: 4,
-			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 56, 0, time.UTC).Local()},
-		},
-		SourceImport: &Rate{
-			Limit:     6,
-			Remaining: 5,
-			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 57, 0, time.UTC).Local()},
-		},
-		CodeScanningUpload: &Rate{
-			Limit:     7,
-			Remaining: 6,
-			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 58, 0, time.UTC).Local()},
-		},
-		ActionsRunnerRegistration: &Rate{
-			Limit:     8,
-			Remaining: 7,
-			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 59, 0, time.UTC).Local()},
-		},
-		SCIM: &Rate{
-			Limit:     9,
-			Remaining: 8,
-			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 48, 00, 0, time.UTC).Local()},
-		},
-	}
-	if !cmp.Equal(rate, want) {
-		t.Errorf("RateLimits returned %+v, want %+v", rate, want)
-	}
-	tests := []struct {
-		category rateLimitCategory
-		rate     *Rate
-	}{
-		{
-			category: coreCategory,
-			rate:     want.Core,
-		},
-		{
-			category: searchCategory,
-			rate:     want.Search,
-		},
-		{
-			category: graphqlCategory,
-			rate:     want.GraphQL,
-		},
-		{
-			category: integrationManifestCategory,
-			rate:     want.IntegrationManifest,
-		},
-		{
-			category: sourceImportCategory,
-			rate:     want.SourceImport,
-		},
-		{
-			category: codeScanningUploadCategory,
-			rate:     want.CodeScanningUpload,
-		},
-		{
-			category: actionsRunnerRegistrationCategory,
-			rate:     want.ActionsRunnerRegistration,
-		},
-		{
-			category: scimCategory,
-			rate:     want.SCIM,
-		},
-	}
-
-	for _, tt := range tests {
-		if got, want := client.rateLimits[tt.category], *tt.rate; got != want {
-			t.Errorf("client.rateLimits[%v] is %+v, want %+v", tt.category, got, want)
-		}
-	}
-}
-
-func TestRateLimits_coverage(t *testing.T) {
-	client, _, _, teardown := setup()
-	defer teardown()
-
-	ctx := context.Background()
-
-	const methodName = "RateLimits"
-	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
-		_, resp, err := client.RateLimits(ctx)
-		return resp, err
-	})
-}
-
-func TestRateLimits_overQuota(t *testing.T) {
-	client, mux, _, teardown := setup()
-	defer teardown()
-
-	client.rateLimits[coreCategory] = Rate{
-		Limit:     1,
-		Remaining: 0,
-		Reset:     Timestamp{time.Now().Add(time.Hour).Local()},
-	}
-	mux.HandleFunc("/rate_limit", func(w http.ResponseWriter, r *http.Request) {
-		fmt.Fprint(w, `{"resources":{
-			"core": {"limit":2,"remaining":1,"reset":1372700873},
-			"search": {"limit":3,"remaining":2,"reset":1372700874},
-			"graphql": {"limit":4,"remaining":3,"reset":1372700875},
-			"integration_manifest": {"limit":5,"remaining":4,"reset":1372700876},
-			"source_import": {"limit":6,"remaining":5,"reset":1372700877},
-			"code_scanning_upload": {"limit":7,"remaining":6,"reset":1372700878},
-			"actions_runner_registration": {"limit":8,"remaining":7,"reset":1372700879},
-			"scim": {"limit":9,"remaining":8,"reset":1372700880}
-		}}`)
-	})
-
-	ctx := context.Background()
-	rate, _, err := client.RateLimits(ctx)
-	if err != nil {
-		t.Errorf("RateLimits returned error: %v", err)
-	}
-
-	want := &RateLimits{
-		Core: &Rate{
-			Limit:     2,
-			Remaining: 1,
-			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 53, 0, time.UTC).Local()},
-		},
-		Search: &Rate{
-			Limit:     3,
-			Remaining: 2,
-			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 54, 0, time.UTC).Local()},
-		},
-		GraphQL: &Rate{
-			Limit:     4,
-			Remaining: 3,
-			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 55, 0, time.UTC).Local()},
-		},
-		IntegrationManifest: &Rate{
-			Limit:     5,
-			Remaining: 4,
-			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 56, 0, time.UTC).Local()},
-		},
-		SourceImport: &Rate{
-			Limit:     6,
-			Remaining: 5,
-			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 57, 0, time.UTC).Local()},
-		},
-		CodeScanningUpload: &Rate{
-			Limit:     7,
-			Remaining: 6,
-			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 58, 0, time.UTC).Local()},
-		},
-		ActionsRunnerRegistration: &Rate{
-			Limit:     8,
-			Remaining: 7,
-			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 59, 0, time.UTC).Local()},
-		},
-		SCIM: &Rate{
-			Limit:     9,
-			Remaining: 8,
-			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 48, 00, 0, time.UTC).Local()},
-		},
-	}
-	if !cmp.Equal(rate, want) {
-		t.Errorf("RateLimits returned %+v, want %+v", rate, want)
-	}
-
-	tests := []struct {
-		category rateLimitCategory
-		rate     *Rate
-	}{
-		{
-			category: coreCategory,
-			rate:     want.Core,
-		},
-		{
-			category: searchCategory,
-			rate:     want.Search,
-		},
-		{
-			category: graphqlCategory,
-			rate:     want.GraphQL,
-		},
-		{
-			category: integrationManifestCategory,
-			rate:     want.IntegrationManifest,
-		},
-		{
-			category: sourceImportCategory,
-			rate:     want.SourceImport,
-		},
-		{
-			category: codeScanningUploadCategory,
-			rate:     want.CodeScanningUpload,
-		},
-		{
-			category: actionsRunnerRegistrationCategory,
-			rate:     want.ActionsRunnerRegistration,
-		},
-		{
-			category: scimCategory,
-			rate:     want.SCIM,
-		},
-	}
-	for _, tt := range tests {
-		if got, want := client.rateLimits[tt.category], *tt.rate; got != want {
-			t.Errorf("client.rateLimits[%v] is %+v, want %+v", tt.category, got, want)
-		}
-	}
-}
-
 func TestSetCredentialsAsHeaders(t *testing.T) {
 	req := new(http.Request)
 	id, secret := "id", "secret"
@@ -2770,116 +2508,6 @@ func TestError_Marshal(t *testing.T) {
 	testJSONMarshal(t, u, want)
 }
 
-func TestRate_Marshal(t *testing.T) {
-	testJSONMarshal(t, &Rate{}, "{}")
-
-	u := &Rate{
-		Limit:     1,
-		Remaining: 1,
-		Reset:     Timestamp{referenceTime},
-	}
-
-	want := `{
-		"limit": 1,
-		"remaining": 1,
-		"reset": ` + referenceTimeStr + `
-	}`
-
-	testJSONMarshal(t, u, want)
-}
-
-func TestRateLimits_Marshal(t *testing.T) {
-	testJSONMarshal(t, &RateLimits{}, "{}")
-
-	u := &RateLimits{
-		Core: &Rate{
-			Limit:     1,
-			Remaining: 1,
-			Reset:     Timestamp{referenceTime},
-		},
-		Search: &Rate{
-			Limit:     1,
-			Remaining: 1,
-			Reset:     Timestamp{referenceTime},
-		},
-		GraphQL: &Rate{
-			Limit:     1,
-			Remaining: 1,
-			Reset:     Timestamp{referenceTime},
-		},
-		IntegrationManifest: &Rate{
-			Limit:     1,
-			Remaining: 1,
-			Reset:     Timestamp{referenceTime},
-		},
-		SourceImport: &Rate{
-			Limit:     1,
-			Remaining: 1,
-			Reset:     Timestamp{referenceTime},
-		},
-		CodeScanningUpload: &Rate{
-			Limit:     1,
-			Remaining: 1,
-			Reset:     Timestamp{referenceTime},
-		},
-		ActionsRunnerRegistration: &Rate{
-			Limit:     1,
-			Remaining: 1,
-			Reset:     Timestamp{referenceTime},
-		},
-		SCIM: &Rate{
-			Limit:     1,
-			Remaining: 1,
-			Reset:     Timestamp{referenceTime},
-		},
-	}
-
-	want := `{
-		"core": {
-			"limit": 1,
-			"remaining": 1,
-			"reset": ` + referenceTimeStr + `
-		},
-		"search": {
-			"limit": 1,
-			"remaining": 1,
-			"reset": ` + referenceTimeStr + `
-		},
-		"graphql": {
-			"limit": 1,
-			"remaining": 1,
-			"reset": ` + referenceTimeStr + `
-		},
-		"integration_manifest": {
-			"limit": 1,
-			"remaining": 1,
-			"reset": ` + referenceTimeStr + `
-		},
-		"source_import": {
-			"limit": 1,
-			"remaining": 1,
-			"reset": ` + referenceTimeStr + `
-		},
-		"code_scanning_upload": {
-			"limit": 1,
-			"remaining": 1,
-			"reset": ` + referenceTimeStr + `
-		},
-		"actions_runner_registration": {
-			"limit": 1,
-			"remaining": 1,
-			"reset": ` + referenceTimeStr + `
-		},
-		"scim": {
-			"limit": 1,
-			"remaining": 1,
-			"reset": ` + referenceTimeStr + `
-		}
-	}`
-
-	testJSONMarshal(t, u, want)
-}
-
 func TestParseTokenExpiration(t *testing.T) {
 	tests := []struct {
 		header string
diff --git a/github/rate_limit_test.go b/github/rate_limit_test.go
new file mode 100644
index 00000000..167288bc
--- /dev/null
+++ b/github/rate_limit_test.go
@@ -0,0 +1,388 @@
+// Copyright 2023 The go-github AUTHORS. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package github
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestRateLimits_String(t *testing.T) {
+	v := RateLimits{
+		Core:                      &Rate{},
+		Search:                    &Rate{},
+		GraphQL:                   &Rate{},
+		IntegrationManifest:       &Rate{},
+		SourceImport:              &Rate{},
+		CodeScanningUpload:        &Rate{},
+		ActionsRunnerRegistration: &Rate{},
+		SCIM:                      &Rate{},
+	}
+	want := `github.RateLimits{Core:github.Rate{Limit:0, Remaining:0, Reset:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}}, Search:github.Rate{Limit:0, Remaining:0, Reset:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}}, GraphQL:github.Rate{Limit:0, Remaining:0, Reset:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}}, IntegrationManifest:github.Rate{Limit:0, Remaining:0, Reset:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}}, SourceImport:github.Rate{Limit:0, Remaining:0, Reset:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}}, CodeScanningUpload:github.Rate{Limit:0, Remaining:0, Reset:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}}, ActionsRunnerRegistration:github.Rate{Limit:0, Remaining:0, Reset:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}}, SCIM:github.Rate{Limit:0, Remaining:0, Reset:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}}}`
+	if got := v.String(); got != want {
+		t.Errorf("RateLimits.String = %v, want %v", got, want)
+	}
+}
+
+func TestRateLimits(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/rate_limit", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		fmt.Fprint(w, `{"resources":{
+			"core": {"limit":2,"remaining":1,"reset":1372700873},
+			"search": {"limit":3,"remaining":2,"reset":1372700874},
+			"graphql": {"limit":4,"remaining":3,"reset":1372700875},
+			"integration_manifest": {"limit":5,"remaining":4,"reset":1372700876},
+			"source_import": {"limit":6,"remaining":5,"reset":1372700877},
+			"code_scanning_upload": {"limit":7,"remaining":6,"reset":1372700878},
+			"actions_runner_registration": {"limit":8,"remaining":7,"reset":1372700879},
+			"scim": {"limit":9,"remaining":8,"reset":1372700880}
+		}}`)
+	})
+
+	ctx := context.Background()
+	rate, _, err := client.RateLimit.Get(ctx)
+	if err != nil {
+		t.Errorf("RateLimits returned error: %v", err)
+	}
+
+	want := &RateLimits{
+		Core: &Rate{
+			Limit:     2,
+			Remaining: 1,
+			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 53, 0, time.UTC).Local()},
+		},
+		Search: &Rate{
+			Limit:     3,
+			Remaining: 2,
+			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 54, 0, time.UTC).Local()},
+		},
+		GraphQL: &Rate{
+			Limit:     4,
+			Remaining: 3,
+			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 55, 0, time.UTC).Local()},
+		},
+		IntegrationManifest: &Rate{
+			Limit:     5,
+			Remaining: 4,
+			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 56, 0, time.UTC).Local()},
+		},
+		SourceImport: &Rate{
+			Limit:     6,
+			Remaining: 5,
+			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 57, 0, time.UTC).Local()},
+		},
+		CodeScanningUpload: &Rate{
+			Limit:     7,
+			Remaining: 6,
+			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 58, 0, time.UTC).Local()},
+		},
+		ActionsRunnerRegistration: &Rate{
+			Limit:     8,
+			Remaining: 7,
+			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 59, 0, time.UTC).Local()},
+		},
+		SCIM: &Rate{
+			Limit:     9,
+			Remaining: 8,
+			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 48, 00, 0, time.UTC).Local()},
+		},
+	}
+	if !cmp.Equal(rate, want) {
+		t.Errorf("RateLimits returned %+v, want %+v", rate, want)
+	}
+	tests := []struct {
+		category rateLimitCategory
+		rate     *Rate
+	}{
+		{
+			category: coreCategory,
+			rate:     want.Core,
+		},
+		{
+			category: searchCategory,
+			rate:     want.Search,
+		},
+		{
+			category: graphqlCategory,
+			rate:     want.GraphQL,
+		},
+		{
+			category: integrationManifestCategory,
+			rate:     want.IntegrationManifest,
+		},
+		{
+			category: sourceImportCategory,
+			rate:     want.SourceImport,
+		},
+		{
+			category: codeScanningUploadCategory,
+			rate:     want.CodeScanningUpload,
+		},
+		{
+			category: actionsRunnerRegistrationCategory,
+			rate:     want.ActionsRunnerRegistration,
+		},
+		{
+			category: scimCategory,
+			rate:     want.SCIM,
+		},
+	}
+
+	for _, tt := range tests {
+		if got, want := client.rateLimits[tt.category], *tt.rate; got != want {
+			t.Errorf("client.rateLimits[%v] is %+v, want %+v", tt.category, got, want)
+		}
+	}
+}
+
+func TestRateLimits_coverage(t *testing.T) {
+	client, _, _, teardown := setup()
+	defer teardown()
+
+	ctx := context.Background()
+
+	const methodName = "RateLimits"
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		_, resp, err := client.RateLimit.Get(ctx)
+		return resp, err
+	})
+}
+
+func TestRateLimits_overQuota(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	client.rateLimits[coreCategory] = Rate{
+		Limit:     1,
+		Remaining: 0,
+		Reset:     Timestamp{time.Now().Add(time.Hour).Local()},
+	}
+	mux.HandleFunc("/rate_limit", func(w http.ResponseWriter, r *http.Request) {
+		fmt.Fprint(w, `{"resources":{
+			"core": {"limit":2,"remaining":1,"reset":1372700873},
+			"search": {"limit":3,"remaining":2,"reset":1372700874},
+			"graphql": {"limit":4,"remaining":3,"reset":1372700875},
+			"integration_manifest": {"limit":5,"remaining":4,"reset":1372700876},
+			"source_import": {"limit":6,"remaining":5,"reset":1372700877},
+			"code_scanning_upload": {"limit":7,"remaining":6,"reset":1372700878},
+			"actions_runner_registration": {"limit":8,"remaining":7,"reset":1372700879},
+			"scim": {"limit":9,"remaining":8,"reset":1372700880}
+		}}`)
+	})
+
+	ctx := context.Background()
+	rate, _, err := client.RateLimit.Get(ctx)
+	if err != nil {
+		t.Errorf("RateLimits returned error: %v", err)
+	}
+
+	want := &RateLimits{
+		Core: &Rate{
+			Limit:     2,
+			Remaining: 1,
+			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 53, 0, time.UTC).Local()},
+		},
+		Search: &Rate{
+			Limit:     3,
+			Remaining: 2,
+			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 54, 0, time.UTC).Local()},
+		},
+		GraphQL: &Rate{
+			Limit:     4,
+			Remaining: 3,
+			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 55, 0, time.UTC).Local()},
+		},
+		IntegrationManifest: &Rate{
+			Limit:     5,
+			Remaining: 4,
+			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 56, 0, time.UTC).Local()},
+		},
+		SourceImport: &Rate{
+			Limit:     6,
+			Remaining: 5,
+			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 57, 0, time.UTC).Local()},
+		},
+		CodeScanningUpload: &Rate{
+			Limit:     7,
+			Remaining: 6,
+			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 58, 0, time.UTC).Local()},
+		},
+		ActionsRunnerRegistration: &Rate{
+			Limit:     8,
+			Remaining: 7,
+			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 47, 59, 0, time.UTC).Local()},
+		},
+		SCIM: &Rate{
+			Limit:     9,
+			Remaining: 8,
+			Reset:     Timestamp{time.Date(2013, time.July, 1, 17, 48, 00, 0, time.UTC).Local()},
+		},
+	}
+	if !cmp.Equal(rate, want) {
+		t.Errorf("RateLimits returned %+v, want %+v", rate, want)
+	}
+
+	tests := []struct {
+		category rateLimitCategory
+		rate     *Rate
+	}{
+		{
+			category: coreCategory,
+			rate:     want.Core,
+		},
+		{
+			category: searchCategory,
+			rate:     want.Search,
+		},
+		{
+			category: graphqlCategory,
+			rate:     want.GraphQL,
+		},
+		{
+			category: integrationManifestCategory,
+			rate:     want.IntegrationManifest,
+		},
+		{
+			category: sourceImportCategory,
+			rate:     want.SourceImport,
+		},
+		{
+			category: codeScanningUploadCategory,
+			rate:     want.CodeScanningUpload,
+		},
+		{
+			category: actionsRunnerRegistrationCategory,
+			rate:     want.ActionsRunnerRegistration,
+		},
+		{
+			category: scimCategory,
+			rate:     want.SCIM,
+		},
+	}
+	for _, tt := range tests {
+		if got, want := client.rateLimits[tt.category], *tt.rate; got != want {
+			t.Errorf("client.rateLimits[%v] is %+v, want %+v", tt.category, got, want)
+		}
+	}
+}
+
+func TestRateLimits_Marshal(t *testing.T) {
+	testJSONMarshal(t, &RateLimits{}, "{}")
+
+	u := &RateLimits{
+		Core: &Rate{
+			Limit:     1,
+			Remaining: 1,
+			Reset:     Timestamp{referenceTime},
+		},
+		Search: &Rate{
+			Limit:     1,
+			Remaining: 1,
+			Reset:     Timestamp{referenceTime},
+		},
+		GraphQL: &Rate{
+			Limit:     1,
+			Remaining: 1,
+			Reset:     Timestamp{referenceTime},
+		},
+		IntegrationManifest: &Rate{
+			Limit:     1,
+			Remaining: 1,
+			Reset:     Timestamp{referenceTime},
+		},
+		SourceImport: &Rate{
+			Limit:     1,
+			Remaining: 1,
+			Reset:     Timestamp{referenceTime},
+		},
+		CodeScanningUpload: &Rate{
+			Limit:     1,
+			Remaining: 1,
+			Reset:     Timestamp{referenceTime},
+		},
+		ActionsRunnerRegistration: &Rate{
+			Limit:     1,
+			Remaining: 1,
+			Reset:     Timestamp{referenceTime},
+		},
+		SCIM: &Rate{
+			Limit:     1,
+			Remaining: 1,
+			Reset:     Timestamp{referenceTime},
+		},
+	}
+
+	want := `{
+		"core": {
+			"limit": 1,
+			"remaining": 1,
+			"reset": ` + referenceTimeStr + `
+		},
+		"search": {
+			"limit": 1,
+			"remaining": 1,
+			"reset": ` + referenceTimeStr + `
+		},
+		"graphql": {
+			"limit": 1,
+			"remaining": 1,
+			"reset": ` + referenceTimeStr + `
+		},
+		"integration_manifest": {
+			"limit": 1,
+			"remaining": 1,
+			"reset": ` + referenceTimeStr + `
+		},
+		"source_import": {
+			"limit": 1,
+			"remaining": 1,
+			"reset": ` + referenceTimeStr + `
+		},
+		"code_scanning_upload": {
+			"limit": 1,
+			"remaining": 1,
+			"reset": ` + referenceTimeStr + `
+		},
+		"actions_runner_registration": {
+			"limit": 1,
+			"remaining": 1,
+			"reset": ` + referenceTimeStr + `
+		},
+		"scim": {
+			"limit": 1,
+			"remaining": 1,
+			"reset": ` + referenceTimeStr + `
+		}
+	}`
+
+	testJSONMarshal(t, u, want)
+}
+
+func TestRate_Marshal(t *testing.T) {
+	testJSONMarshal(t, &Rate{}, "{}")
+
+	u := &Rate{
+		Limit:     1,
+		Remaining: 1,
+		Reset:     Timestamp{referenceTime},
+	}
+
+	want := `{
+		"limit": 1,
+		"remaining": 1,
+		"reset": ` + referenceTimeStr + `
+	}`
+
+	testJSONMarshal(t, u, want)
+}
diff --git a/test/integration/misc_test.go b/test/integration/misc_test.go
index 6ffb163f..0e9ef6e0 100644
--- a/test/integration/misc_test.go
+++ b/test/integration/misc_test.go
@@ -49,7 +49,7 @@ func TestAPIMeta(t *testing.T) {
 }
 
 func TestRateLimits(t *testing.T) {
-	limits, _, err := client.RateLimits(context.Background())
+	limits, _, err := client.RateLimit.Get(context.Background())
 	if err != nil {
 		t.Fatalf("RateLimits returned error: %v", err)
 	}