├── .editorconfig ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── build.yml │ └── unittests.yml ├── .gitignore ├── Directory.Build.props ├── Directory.Build.targets ├── GitHubApiStatus.sln ├── GitStatus.Common ├── Constants │ └── GitHubConstants.cs └── GitStatus.Common.csproj ├── LICENSE ├── README.md ├── global.json ├── samples ├── GitStatus.API │ ├── .gitignore │ ├── GetRateLimits.cs │ ├── GitStatus.API.csproj │ ├── Program.cs │ └── host.json ├── GitStatus.ConsoleApp │ ├── GitStatus.ConsoleApp.csproj │ └── Program.cs ├── GitStatus.Mobile │ ├── .meteor │ │ └── generated │ │ │ ├── CommunityToolkit.Maui.json │ │ │ ├── GitStatus.Mobile.json │ │ │ ├── Microsoft.Maui.Controls.Compatibility.json │ │ │ └── Microsoft.Maui.Controls.json │ ├── App.cs │ ├── AppShell.cs │ ├── GitStatus.Mobile.csproj │ ├── MauiProgram.cs │ ├── Pages │ │ ├── Base │ │ │ ├── BaseContentPage.cs │ │ │ └── BaseStatusPage.cs │ │ ├── GraphQLApiStatusPage.cs │ │ └── RestApiStatusPage.cs │ ├── Platforms │ │ ├── Android │ │ │ ├── AndroidManifest.xml │ │ │ ├── MainActivity.cs │ │ │ ├── MainApplication.cs │ │ │ └── Resources │ │ │ │ └── values │ │ │ │ └── colors.xml │ │ ├── MacCatalyst │ │ │ ├── AppDelegate.cs │ │ │ ├── Info.plist │ │ │ └── Program.cs │ │ ├── Tizen │ │ │ ├── Main.cs │ │ │ └── tizen-manifest.xml │ │ ├── Windows │ │ │ ├── App.xaml │ │ │ ├── App.xaml.cs │ │ │ ├── Package.appxmanifest │ │ │ └── app.manifest │ │ └── iOS │ │ │ ├── AppDelegate.cs │ │ │ ├── Info.plist │ │ │ └── Program.cs │ ├── Resources │ │ ├── AppIcon │ │ │ ├── appicon.svg │ │ │ └── appiconfg.svg │ │ ├── Fonts │ │ │ ├── OpenSans-Regular.ttf │ │ │ └── OpenSans-Semibold.ttf │ │ ├── Images │ │ │ └── dotnet_bot.svg │ │ ├── Raw │ │ │ └── AboutAssets.txt │ │ ├── Splash │ │ │ └── splash.svg │ │ └── Styles │ │ │ ├── Colors.xaml │ │ │ └── Styles.xaml │ └── ViewModels │ │ ├── Base │ │ ├── BaseStatusViewModel.cs │ │ └── BaseViewModel.cs │ │ ├── GraphQLApiStatusViewModel.cs │ │ └── RestApiStatusViewModel.cs └── GitStatus.Web │ ├── App.razor │ ├── GitStatus.Web.csproj │ ├── Pages │ ├── GraphQL.razor │ ├── Index.razor │ └── Rest.razor │ ├── Program.cs │ ├── Shared │ ├── MainLayout.razor │ ├── MainLayout.razor.css │ ├── NavMenu.razor │ └── NavMenu.razor.css │ ├── _Imports.razor │ └── wwwroot │ ├── css │ ├── app.css │ ├── bootstrap │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.css.map │ └── open-iconic │ │ ├── FONT-LICENSE │ │ ├── ICON-LICENSE │ │ ├── README.md │ │ └── font │ │ ├── css │ │ └── open-iconic-bootstrap.min.css │ │ └── fonts │ │ ├── open-iconic.eot │ │ ├── open-iconic.otf │ │ ├── open-iconic.svg │ │ ├── open-iconic.ttf │ │ └── open-iconic.woff │ ├── favicon.ico │ ├── icon-512.png │ ├── index.html │ ├── manifest.json │ ├── sample-data │ └── weather.json │ ├── service-worker.js │ └── service-worker.published.js └── src ├── GitHubApiStatus.Extensions.UnitTests ├── GitHubApiStatus.Extensions.UnitTests.csproj ├── GitHubApiStatusServiceExtensionsTests.cs └── MockGitHubApiStatusService.cs ├── GitHubApiStatus.Extensions ├── .editorconfig ├── GitHubApiStatus.Extensions.csproj ├── GitHubApiStatus.Extensions.snk ├── GitHubApiStatusServiceExtensions.cs └── README.md ├── GitHubApiStatus.UnitTests ├── GitHubApiStatus.UnitTests.csproj ├── Models │ ├── GraphQLError.cs │ ├── GraphQLRequest.cs │ └── GraphQLResponse.cs └── Tests │ ├── Base │ └── BaseTest.cs │ ├── GetApiRateLimitsTests_NoCancellationToken.cs │ ├── GetApiRateLimitsTests_WithCancellationToken.cs │ ├── GetRateLimitRestDateTimeTests.cs │ ├── GetRateLimitTests.cs │ ├── GetRateLimitTimeRemainingTests.cs │ ├── GetRemainingRequestCountTests.cs │ ├── GitHubApiStatusServiceConstructorTests.cs │ ├── HasReachedMaximumApiCallLimitTests.cs │ ├── HttpResponseHeadersExtensionsTests.cs │ ├── IsAbuseRateLimitTest.cs │ ├── IsResponseFromAuthenticatedRequestTests.cs │ ├── ProductHeaderValueTests.cs │ └── SetAuthenticationHeaderValueTests.cs └── GitHubApiStatus ├── .editorconfig ├── GitHubApiStatus.csproj ├── GitHubApiStatus.snk ├── GitHubApiStatusException.cs ├── HttpResponseHeadersExtensions.cs ├── Interfaces ├── IGitHubApiRateLimitResponse.cs ├── IGitHubApiRateLimits.cs ├── IGitHubApiStatusService.cs └── IRateLimitStatus.cs ├── Models ├── GitHubApiRateLimits.cs ├── GitHubApiRateLimitsMutable.cs └── RateLimitStatus.cs ├── README.md └── Services └── GitHubApiStatusService.cs /.editorconfig: -------------------------------------------------------------------------------- 1 | # Suppress: EC112 2 | # top-most EditorConfig file 3 | root = true 4 | 5 | # Default settings: 6 | # A newline ending every file 7 | # Use 4 spaces as indentation 8 | [*] 9 | insert_final_newline = false 10 | indent_style = space 11 | indent_size = 4 12 | 13 | # Code files 14 | [*.{cs,csx,vb,vbx}] 15 | indent_style = tab 16 | indent_size = 4 17 | 18 | # Code files 19 | [*.sln] 20 | indent_size = 4 21 | 22 | # Xml project files 23 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] 24 | indent_size = 2 25 | 26 | # Xml config files 27 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] 28 | indent_size = 2 29 | 30 | # JSON files 31 | [*.json] 32 | indent_size = 2 33 | 34 | # XML files 35 | [*.xml] 36 | indent_size = 2 37 | 38 | [*.cs] 39 | 40 | # Organize usings 41 | dotnet_sort_system_directives_first = true 42 | 43 | # CS4014: Because this call is not awaited, execution of the current method continues before the call is completed 44 | dotnet_diagnostic.CS4014.severity = error 45 | 46 | # CS2012: ValueTask instances returned from member invocations are intended to be directly awaited. Attempts to consume a ValueTask multiple times or to directly access one's result before it's known to be completed may result in an exception or corruption. Ignoring such a ValueTask is likely an indication of a functional bug and may degrade performance. 47 | dotnet_diagnostic.CS2012.severity = error 48 | 49 | # CS1998 : This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread. 50 | dotnet_diagnostic.CS1998.severity = error 51 | 52 | # Remove explicit default access modifiers 53 | dotnet_style_require_accessibility_modifiers = omit_if_default:error 54 | 55 | # CA1063: Implement IDisposable Correctly 56 | dotnet_diagnostic.CA1063.severity = error 57 | 58 | # CA1001: Type owns disposable field(s) but is not disposable 59 | dotnet_diagnostic.CA1001.severity = error 60 | 61 | # Pattern matching 62 | dotnet_style_object_initializer = true:suggestion 63 | dotnet_style_collection_initializer = true:suggestion 64 | dotnet_style_coalesce_expression = true:suggestion 65 | dotnet_style_null_propagation = true:suggestion 66 | dotnet_style_explicit_tuple_names = true:suggestion 67 | dotnet_style_prefer_is_null_check_over_reference_equality_method=true:suggestion 68 | 69 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 70 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 71 | csharp_style_inlined_variable_declaration = true:suggestion 72 | csharp_style_throw_expression = true:suggestion 73 | csharp_style_conditional_delegate_call = true:suggestion 74 | 75 | # Collection Expressions 76 | dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion 77 | 78 | # Naming rules 79 | 80 | dotnet_diagnostic.IDE1006.severity = error 81 | 82 | ## Public Fields are kept Pascal Case 83 | dotnet_naming_symbols.public_symbols.applicable_kinds = field 84 | dotnet_naming_symbols.public_symbols.applicable_accessibilities = public, internal 85 | 86 | dotnet_naming_style.first_word_upper_case_style.capitalization = first_word_upper 87 | 88 | dotnet_naming_rule.public_members_must_be_capitalized.symbols = public_symbols 89 | dotnet_naming_rule.public_members_must_be_capitalized.style = first_word_upper_case_style 90 | dotnet_naming_rule.public_members_must_be_capitalized.severity = suggestion 91 | 92 | ## Instance fields are camelCase 93 | dotnet_naming_rule.instance_fields_should_be_camel_case.severity = error 94 | dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields 95 | dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style 96 | 97 | dotnet_naming_symbols.instance_fields.applicable_kinds = field 98 | 99 | dotnet_naming_style.instance_field_style.capitalization = camel_case 100 | dotnet_naming_style.instance_field_style.required_prefix = _ 101 | 102 | ## Static fields are camelCase 103 | dotnet_naming_rule.static_fields_should_be_camel_case.severity = error 104 | dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields 105 | dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style 106 | 107 | dotnet_naming_symbols.static_fields.applicable_kinds = field 108 | dotnet_naming_symbols.static_fields.required_modifiers = static 109 | dotnet_naming_symbols.static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected 110 | 111 | dotnet_naming_style.static_field_style.capitalization = camel_case 112 | dotnet_naming_style.static_field_style.required_prefix = _ 113 | 114 | # Modifier preferences 115 | csharp_prefer_static_local_function = true:suggestion 116 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:error 117 | 118 | # CA1822: Member does not access instance data and can be marked as static 119 | dotnet_diagnostic.CA1822.severity = suggestion 120 | 121 | # CA1050: Declare types in namespaces 122 | dotnet_diagnostic.CA1050.severity = error 123 | 124 | # CA2016: Forward the 'cancellationToken' parameter methods that take one 125 | dotnet_diagnostic.CA2016.severity = error 126 | 127 | # CA1068: CancellationToken parameters must come last 128 | dotnet_diagnostic.CA1068.severity = error 129 | 130 | # CA2208: Method passes parameter as the paramName argument to a ArgumentNullException constructor. Replace this argument with one of the method's parameter names. Note that the provided parameter name should have the exact casing as declared on the method. 131 | dotnet_diagnostic.CA2208.severity = error 132 | 133 | # CA1834: Use 'StringBuilder.Append(char)' instead of 'StringBuilder.Append(string)' when the input is a constant unit string 134 | dotnet_diagnostic.CA1834.severity = error 135 | 136 | # IDE0220: Add explicit cast 137 | dotnet_diagnostic.IDE0220.severity = error -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [brminnick] 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "nuget" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Projects 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | pull_request: 8 | branches: 9 | - "*" 10 | 11 | jobs: 12 | Android: 13 | runs-on: macos-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Setup .NET v9.0 19 | uses: actions/setup-dotnet@v4 20 | with: 21 | dotnet-version: '9.0.x' 22 | 23 | - name: Install .NET MAUI Workload 24 | run: | 25 | dotnet workload install maui 26 | 27 | - name: Build Android App 28 | run: | 29 | MobileProject=`find . -name GitStatus.Mobile.csproj` 30 | echo $MobileProject 31 | 32 | MobileProjectDirectory=`dirname $MobileProject` 33 | echo $MobileProjectDirectory 34 | 35 | dotnet build $MobileProjectDirectory -f:net9.0-android -c Release 36 | 37 | iOS: 38 | runs-on: macos-15 39 | 40 | steps: 41 | - uses: actions/checkout@v4 42 | 43 | - name: Setup .NET v9.0 44 | uses: actions/setup-dotnet@v4 45 | with: 46 | dotnet-version: '9.0.x' 47 | 48 | - name: Install .NET MAUI Workload 49 | run: | 50 | dotnet workload install maui 51 | 52 | - name: Install Xcode 53 | uses: maxim-lobanov/setup-xcode@v1 54 | with: 55 | xcode-version: latest-stable 56 | 57 | - name: Build iOS App 58 | run: | 59 | MobileProject=`find . -name GitStatus.Mobile.csproj` 60 | echo $MobileProject 61 | 62 | MobileProjectDirectory=`dirname $MobileProject` 63 | echo $MobileProjectDirectory 64 | 65 | dotnet build $MobileProjectDirectory -f:net9.0-ios -c Release 66 | 67 | MacCatalyst: 68 | runs-on: macos-15 69 | 70 | steps: 71 | - uses: actions/checkout@v4 72 | 73 | - name: Setup .NET v9.0 74 | uses: actions/setup-dotnet@v4 75 | with: 76 | dotnet-version: '9.0.x' 77 | 78 | - name: Install .NET MAUI Workload 79 | run: | 80 | dotnet workload install maui 81 | 82 | - name: Install Xcode 83 | uses: maxim-lobanov/setup-xcode@v1 84 | with: 85 | xcode-version: latest-stable 86 | 87 | - name: Build macOS App 88 | run: | 89 | MobileProject=`find . -name GitStatus.Mobile.csproj` 90 | echo $MobileProject 91 | 92 | MobileProjectDirectory=`dirname $MobileProject` 93 | echo $MobileProjectDirectory 94 | 95 | dotnet build $MobileProjectDirectory -f:net9.0-maccatalyst -c Release 96 | 97 | Windows: 98 | runs-on: windows-latest 99 | 100 | steps: 101 | - uses: actions/checkout@v4 102 | 103 | - uses: actions/setup-java@v4 104 | with: 105 | distribution: 'microsoft' 106 | java-version: '17' 107 | 108 | - name: Setup .NET v9.0 109 | uses: actions/setup-dotnet@v4 110 | with: 111 | dotnet-version: '9.0.x' 112 | 113 | - name: Install .NET MAUI Workload 114 | run: | 115 | dotnet workload install maui 116 | 117 | - name: Build Windows App 118 | run: | 119 | dotnet build ./samples/GitStatus.Mobile/ -c Release -f net9.0-windows10.0.19041.0 120 | 121 | API: 122 | runs-on: macos-latest 123 | 124 | steps: 125 | - uses: actions/checkout@v4 126 | 127 | - name: Setup .NET v9.0 128 | uses: actions/setup-dotnet@v4 129 | with: 130 | dotnet-version: '9.0.x' 131 | 132 | - name: Build API App 133 | run: | 134 | APIProject=`find . -name GitStatus.API.csproj` 135 | echo $APIProject 136 | 137 | APIProjectDirectory=`dirname $APIProject` 138 | echo $APIProjectDirectory 139 | 140 | dotnet build -c Release $APIProjectDirectory 141 | 142 | Console: 143 | runs-on: macos-latest 144 | 145 | steps: 146 | - uses: actions/checkout@v4 147 | 148 | - name: Setup .NET v9.0 149 | uses: actions/setup-dotnet@v4 150 | with: 151 | dotnet-version: '9.0.x' 152 | 153 | - name: Build Console App 154 | run: | 155 | ConsoleProject=`find . -name GitStatus.ConsoleApp.csproj` 156 | echo $ConsoleProject 157 | 158 | ConsoleProjectDirectory=`dirname $ConsoleProject` 159 | echo $ConsoleProjectDirectory 160 | 161 | dotnet build -c Release $ConsoleProjectDirectory 162 | 163 | Web: 164 | runs-on: macos-latest 165 | 166 | steps: 167 | - uses: actions/checkout@v4 168 | 169 | - name: Setup .NET v9.0 170 | uses: actions/setup-dotnet@v4 171 | with: 172 | dotnet-version: '9.0.x' 173 | 174 | - name: Build Web App 175 | run: | 176 | WebProject=`find . -name GitStatus.Web.csproj` 177 | echo $WebProject 178 | 179 | WebProjectDirectory=`dirname $WebProject` 180 | echo $WebProjectDirectory 181 | 182 | dotnet build -c Release $WebProjectDirectory 183 | 184 | GitHubApiStatus: 185 | runs-on: macos-latest 186 | 187 | steps: 188 | - uses: actions/checkout@v4 189 | 190 | - name: Setup .NET v9.0 191 | uses: actions/setup-dotnet@v4 192 | with: 193 | dotnet-version: '9.0.x' 194 | 195 | 196 | - name: Build Web App 197 | run: | 198 | GitHubApiStatusExtensionsProject=`find . -name GitHubApiStatus.Extensions.csproj` 199 | echo $GitHubApiStatusExtensionsProject 200 | 201 | GitHubApiStatusExtensionsProjectDirectory=`dirname $GitHubApiStatusExtensionsProject` 202 | echo $GitHubApiStatusExtensionsProjectDirectory 203 | 204 | dotnet build -c Release $GitHubApiStatusExtensionsProjectDirectory 205 | -------------------------------------------------------------------------------- /.github/workflows/unittests.yml: -------------------------------------------------------------------------------- 1 | name: Run Unit Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | pull_request: 8 | branches: 9 | - "*" 10 | 11 | jobs: 12 | UnitTests: 13 | runs-on: macos-15 14 | 15 | env: 16 | GitHubPersonalAccessToken: ${{ secrets.GitHubPersonalAccessToken }} 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Install .NET 22 | uses: actions/setup-dotnet@v4 23 | with: 24 | dotnet-version: | 25 | 9.0.x 26 | 8.0.x 27 | 7.0.x 28 | 6.0.x 29 | 3.1.x 30 | 2.1.x 31 | 32 | - name: Inject GitHub Personal Access Token 33 | run: | 34 | GitHubConstantsFile=`find . -name GitHubConstants.cs | head -1` 35 | 36 | echo GitHubConstantsFile = $GitHubConstantsFile 37 | 38 | sed -i '' "s/PersonalAccessToken = \"\"/PersonalAccessToken = \"$GitHubPersonalAccessToken\"/g" "$GitHubConstantsFile" 39 | 40 | echo "Finished Injecting GitHub Personal Access Token" 41 | 42 | - name: Run GitHubApiStatus.UnitTests 43 | run: | 44 | GitHubApiStatusUnitTestProject=`find . -name GitHubApiStatus.UnitTests.csproj` 45 | echo $GitHubApiStatusUnitTestProject 46 | 47 | GitHubApiStatusUnitTestDirectory=`dirname $GitHubApiStatusUnitTestProject` 48 | echo $GitHubApiStatusUnitTestDirectory 49 | 50 | dotnet test $GitHubApiStatusUnitTestDirectory -c 'Release' 51 | 52 | - name: Run GitHubApiStatus.Extensions.UnitTests 53 | run: | 54 | GitHubApiStatusExtensionsUnitTestProject=`find . -name GitHubApiStatus.Extensions.UnitTests.csproj` 55 | echo $GitHubApiStatusExtensionsUnitTestProject 56 | 57 | GitHubApiStatusExtensionsUnitTestDirectory=`dirname $GitHubApiStatusExtensionsUnitTestProject` 58 | echo $GitHubApiStatusExtensionsUnitTestDirectory 59 | 60 | dotnet test $GitHubApiStatusExtensionsUnitTestDirectory -c 'Release' 61 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | enable 5 | 4.0.0 6 | preview 7 | true 8 | enable 9 | net9.0 10 | true 11 | true 12 | true 13 | false 14 | 15 | 16 | 9.0.10 17 | true 18 | true 19 | true 20 | 21 | 22 | enable 23 | all 24 | 25 | 26 | false 27 | false 28 | 29 | 65 | 66 | nullable, 67 | CS0419,CS0618,CS1570,CS1571,CS1572,CS1573,CS1574,CS1580,CS1581,CS1584,CS1587,CS1589,CS1590,CS1591,CS1592,CS1598,CS1658,CS1710,CS1711,CS1712,CS1723,CS1734, 68 | CsWinRT1028,CsWinRT1030, 69 | XC0045,XC0103, 70 | NU1900,NU1901,NU1902,NU1903,NU1904,NU1905, 71 | NUnit1001,NUnit1002,NUnit1003,NUnit1004,NUnit1005,NUnit1006,NUnit1007,NUnit1008,NUnit1009,NUnit1010,NUnit1011,NUnit1012,NUnit1013,NUnit1014,NUnit1015,NUnit1016,NUnit1017,NUnit1018,NUnit1019,NUnit1020,NUnit1021,NUnit1022,NUnit1023,NUnit1024,NUnit1025,NUnit1026,NUnit1027,NUnit1028,NUnit1029,NUnit1030,NUnit1031,NUnit1032,NUnit1033, 72 | NUnit2001,NUnit2002,NUnit2003,NUnit2004,NUnit2005,NUnit2006,NUnit2007,NUnit2008,NUnit2009,NUnit2010,NUnit2011,NUnit2012,NUnit2013,NUnit2014,NUnit2015,NUnit2016,NUnit2017,NUnit2018,NUnit2019,NUnit2020,NUnit2021,NUnit2022,NUnit2023,NUnit2024,NUnit2025,NUnit2026,NUnit2027,NUnit2028,NUnit2029,NUnit2030,NUnit2031,NUnit2032,NUnit2033,NUnit2034,NUnit2035,NUnit2036,NUnit2037,NUnit2038,NUnit2039,NUnit2040,NUnit2041,NUnit2042,NUnit2043,NUnit2044,NUnit2045,NUnit2046,NUnit2047,NUnit2048,NUnit2049,NUnit2050, 73 | NUnit3001,NUnit3002,NUnit3003,NUnit3004, 74 | NUnit4001, 75 | IL2001,IL2002,IL2003,IL2004,IL2005,IL2006,IL2007,IL2008,IL2009, 76 | IL2010,IL2011,IL2012,IL2013,IL2014,IL2015,IL2016,IL2017,IL2018,IL2019, 77 | IL2020,IL2021,IL2022,IL2023,IL2024,IL2025,IL2026,IL2027,IL2028,IL2029, 78 | IL2030,IL2031,IL2032,IL2033,IL2034,IL2035,IL2036,IL2037,IL2038,IL2039, 79 | IL2040,IL2041,IL2042,IL2043,IL2044,IL2045,IL2046,IL2047,IL2048,IL2049, 80 | IL2050,IL2051,IL2052,IL2053,IL2054,IL2055,IL2056,IL2057,IL2058,IL2059, 81 | IL2060,IL2061,IL2062,IL2063,IL2064,IL2065,IL2066,IL2067,IL2068,IL2069, 82 | IL2070,IL2071,IL2072,IL2073,IL2074,IL2075,IL2076,IL2077,IL2078,IL2079, 83 | IL2080,IL2081,IL2082,IL2083,IL2084,IL2085,IL2086,IL2087,IL2088,IL2089, 84 | IL2090,IL2091,IL2092,IL2093,IL2094,IL2095,IL2096,IL2097,IL2098,IL2099, 85 | IL2100,IL2101,IL2102,IL2103,IL2104,IL2105,IL2106,IL2107,IL2108,IL2109, 86 | IL2110,IL2111,IL2112,IL2113,IL2114,IL2115,IL2116,IL2117,IL2118,IL2119, 87 | IL2120,IL2121,IL2122, 88 | IL3050,IL3051,IL3052,IL3053,IL3054,IL3055,IL3056 89 | 90 | 91 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /GitStatus.Common/Constants/GitHubConstants.cs: -------------------------------------------------------------------------------- 1 | namespace GitStatus.Common; 2 | 3 | public static class GitHubConstants 4 | { 5 | public const string AuthScheme = "bearer"; 6 | public const string PersonalAccessToken = ""; 7 | public const string GitHubRestApiUrl = "https://api.github.com"; 8 | public const string GitHubGraphQLApiUrl = "https://api.github.com/graphql"; 9 | } -------------------------------------------------------------------------------- /GitStatus.Common/GitStatus.Common.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0;$(LatestSupportedTFM) 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brandon Minnick 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "9.0.100-rc.2.24474.11", 4 | "rollForward": "latestFeature", 5 | "allowPrerelease": true 6 | } 7 | } -------------------------------------------------------------------------------- /samples/GitStatus.API/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /samples/GitStatus.API/GetRateLimits.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using GitHubApiStatus; 3 | using Microsoft.Azure.Functions.Worker; 4 | using Microsoft.Azure.Functions.Worker.Http; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace GitStatus.API; 8 | 9 | class GitHubApiStatus 10 | { 11 | readonly IGitHubApiStatusService _gitHubApiStatusService; 12 | 13 | public GitHubApiStatus(IGitHubApiStatusService gitHubApiStatusService) => _gitHubApiStatusService = gitHubApiStatusService; 14 | 15 | [Function(nameof(GitHubApiStatus))] 16 | public async Task Run([HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequestData req, FunctionContext context) 17 | { 18 | context.GetLogger().LogInformation("Retrieving Api Rate Limits"); 19 | 20 | var apiStatus = await _gitHubApiStatusService.GetApiRateLimits(CancellationToken.None).ConfigureAwait(false); 21 | 22 | var response = req.CreateResponse(HttpStatusCode.OK); 23 | await response.WriteAsJsonAsync(apiStatus).ConfigureAwait(false); 24 | 25 | return response; 26 | } 27 | } -------------------------------------------------------------------------------- /samples/GitStatus.API/GitStatus.API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(LatestSupportedTFM) 5 | v4 6 | Exe 7 | <_FunctionsSkipCleanOutput>true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | PreserveNewest 17 | 18 | 19 | PreserveNewest 20 | Never 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /samples/GitStatus.API/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using GitHubApiStatus.Extensions; 3 | using GitStatus.Common; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | 8 | namespace GitStatus.API; 9 | 10 | class Program 11 | { 12 | static Task Main(string[] args) 13 | { 14 | var host = new HostBuilder() 15 | .ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddCommandLine(args)) 16 | .ConfigureFunctionsWorkerDefaults() 17 | .ConfigureServices(services => 18 | { 19 | services.AddGitHubApiStatusService(new AuthenticationHeaderValue(GitHubConstants.AuthScheme, GitHubConstants.PersonalAccessToken), new ProductHeaderValue(nameof(GitStatus))) 20 | .ConfigurePrimaryHttpMessageHandler(config => new HttpClientHandler { AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate }); 21 | }) 22 | .Build(); 23 | 24 | return host.RunAsync(); 25 | } 26 | } -------------------------------------------------------------------------------- /samples/GitStatus.API/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingExcludedTypes": "Request", 6 | "samplingSettings": { 7 | "isEnabled": true 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /samples/GitStatus.ConsoleApp/GitStatus.ConsoleApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | $(LatestSupportedTFM) 6 | GitStatus.ConsoleApp 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /samples/GitStatus.ConsoleApp/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using GitHubApiStatus; 3 | using GitStatus.Common; 4 | 5 | namespace GitStatus.ConsoleApp; 6 | 7 | class Program 8 | { 9 | static readonly HttpClient _client = CreateGitHubHttpClient(new AuthenticationHeaderValue(GitHubConstants.AuthScheme, GitHubConstants.PersonalAccessToken), new ProductHeaderValue(nameof(GitStatus))); 10 | static readonly GitHubApiStatusService _gitHubApiStatusService = new(_client); 11 | 12 | static async Task Main(string[] args) 13 | { 14 | var restApiRateLimitDataFromHeaders = await GetRestApiRateLimitDataFromHeaders(); 15 | 16 | Console.WriteLine($"What is the GitHub REST API Rate Limit? {restApiRateLimitDataFromHeaders.RateLimit}"); // What is the GitHub REST API Rate Limit? 5000 17 | Console.WriteLine($"Have I reached the Maximum REST API Limit? {restApiRateLimitDataFromHeaders.HasReachedMaximumApiLimit}"); // Have I reached the Maximum REST API Limit? False 18 | Console.WriteLine($"How many REST API requests do I have remaining? {restApiRateLimitDataFromHeaders.RemainingRequestCount}"); // How many REST API requests do I have remaining? 4956 19 | Console.WriteLine($"How long until the GitHub REST API Rate Limit resets? {restApiRateLimitDataFromHeaders.RateLimitTimeRemaining}"); // How long until the GitHub REST API Rate Limit resets? 00:29:12.4134330 20 | Console.WriteLine($"Did the GitHub REST API Request include a Bearer Token? {restApiRateLimitDataFromHeaders.IsResponseFromAuthenticatedRequest}"); // Did GitHub REST API Request include a Bearer Token? True 21 | 22 | Console.WriteLine(); 23 | 24 | var apiRateLimits = await GetApiRateLimits(); 25 | 26 | // REST API Results 27 | Console.WriteLine($"What is the GitHub REST API Rate Limit? {apiRateLimits.RestApi.RateLimit}"); // What is the GitHub REST API Rate Limit? 5000 28 | Console.WriteLine($"How many REST API requests do I have remaining? {apiRateLimits.RestApi.RemainingRequestCount}"); // How many REST API requests do I have remaining? 4983 29 | Console.WriteLine($"How long until the GitHub REST API Rate Limit resets? {apiRateLimits.RestApi.RateLimitReset_TimeRemaining}"); // How long until the GitHub REST API Rate Limit resets? 00:40:13.8035515 30 | Console.WriteLine($"When does the GitHub REST API Rate Limit reset? {apiRateLimits.RestApi.RateLimitReset_DateTime}"); // When does the GitHub REST API Rate Limit reset? 10/29/2020 3:28:58 AM +00:00 31 | 32 | Console.WriteLine(); 33 | 34 | // GraphQL API Results 35 | Console.WriteLine($"What is the GitHub GraphQL API Rate Limit? {apiRateLimits.GraphQLApi.RateLimit}"); // What is the GitHub GraphQL API Rate Limit? 5000 36 | Console.WriteLine($"How many GraphQL API requests do I have remaining? {apiRateLimits.GraphQLApi.RemainingRequestCount}"); // How many GraphQL API requests do I have remaining? 5000 37 | Console.WriteLine($"How long until the GitHub GraphQL API Rate Limit resets? {apiRateLimits.GraphQLApi.RateLimitReset_TimeRemaining}"); // How long until the GitHub GraphQL API Rate Limit resets? 00:59:59.8034526 38 | Console.WriteLine($"When does the GitHub GraphQL API Rate Limit reset? {apiRateLimits.GraphQLApi.RateLimitReset_DateTime}"); // When does the GitHub GraphQL API Rate Limit reset? 10/29/2020 3:48:44 AM +00:00 39 | 40 | Console.WriteLine(); 41 | 42 | // Search API Results 43 | Console.WriteLine($"What is the GitHub Search API Rate Limit? {apiRateLimits.SearchApi.RateLimit}"); // What is the GitHub Search API Rate Limit? 30 44 | Console.WriteLine($"How many Search API requests do I have remaining? {apiRateLimits.SearchApi.RemainingRequestCount}"); // How many Search API requests do I have remaining? 30 45 | Console.WriteLine($"How long until the GitHub Search API Rate Limit resets? {apiRateLimits.SearchApi.RateLimitReset_TimeRemaining}"); // How long until the GitHub Search API Rate Limit resets? 00:00:59.8034988 46 | Console.WriteLine($"When does the GitHub Search API Rate Limit reset? {apiRateLimits.SearchApi.RateLimitReset_DateTime}"); // When does the GitHub Search API Rate Limit reset? 10/29/2020 2:49:44 AM +00:00 47 | 48 | Console.WriteLine(); 49 | 50 | // Source Import API Results 51 | Console.WriteLine($"What is the GitHub Source Import API Rate Limit? {apiRateLimits.SourceImport.RateLimit}"); // What is the GitHub Source Import API Rate Limit? 100 52 | Console.WriteLine($"How many Source Import API requests do I have remaining? {apiRateLimits.SourceImport.RemainingRequestCount}"); // How many Source Import API requests do I have remaining? 100 53 | Console.WriteLine($"How long until the GitHub Source Import API Rate Limit resets? {apiRateLimits.SourceImport.RateLimitReset_TimeRemaining}"); // How long until the GitHub Source Import API Rate Limit resets? 00:00:59.8034154 54 | Console.WriteLine($"When does the GitHub Source Import API Rate Limit reset? {apiRateLimits.SourceImport.RateLimitReset_DateTime}"); // When does the GitHub Source Import API Rate Limit reset? 10/29/2020 2:49:44 AM +00:00 55 | 56 | Console.WriteLine(); 57 | 58 | // App Manifest Configuration API Results 59 | Console.WriteLine($"What is the GitHub App Manifest Configuration API Rate Limit? {apiRateLimits.AppManifestConfiguration.RateLimit}"); // What is the GitHub App Manifest Configuration API Rate Limit? 5000 60 | Console.WriteLine($"How many App Manifest Configuration API requests do I have remaining? {apiRateLimits.AppManifestConfiguration.RemainingRequestCount}"); // How many App Manifest Configuration API requests do I have remaining? 5000 61 | Console.WriteLine($"How long until the GitHub App Manifest Configuration API Rate Limit resets? {apiRateLimits.AppManifestConfiguration.RateLimitReset_TimeRemaining}"); // How long until the GitHub App Manifest Configuration API Rate Limit resets? 00:59:59.8033802 62 | Console.WriteLine($"When does the GitHub App Manifest Configuration API Rate Limit reset? {apiRateLimits.AppManifestConfiguration.RateLimitReset_DateTime}"); // When does the GitHub App Manifest Configuration API Rate Limit reset? 10/29/2020 3:48:44 AM +00:00 63 | 64 | Console.WriteLine(); 65 | 66 | // Code Scanning Upload API Results 67 | Console.WriteLine($"What is the GitHub Code Scanning Upload API Rate Limit? {apiRateLimits.CodeScanningUpload.RateLimit}"); // What is the GitHub Code Scanning Upload API Rate Limit? 500 68 | Console.WriteLine($"How many Code Scanning Upload API requests do I have remaining? {apiRateLimits.CodeScanningUpload.RemainingRequestCount}"); // How many Code Scanning Upload API requests do I have remaining? 500 69 | Console.WriteLine($"How long until the GitHub Code Scanning Upload API Rate Limit resets? {apiRateLimits.CodeScanningUpload.RateLimitReset_TimeRemaining}"); // How long until the GitHub Code Scanning Upload API Rate Limit resets? 00:59:59.8033455 70 | Console.WriteLine($"When does the GitHub Code Scanning Upload API Rate Limit reset? {apiRateLimits.CodeScanningUpload.RateLimitReset_DateTime}"); // When does the GitHub Code Scanning Upload API Rate Limit reset? 10/29/2020 3:48:44 AM +00:00 71 | 72 | Console.WriteLine(); 73 | } 74 | 75 | static async Task<(TimeSpan RateLimitTimeRemaining, int RateLimit, int RemainingRequestCount, bool IsResponseFromAuthenticatedRequest, bool HasReachedMaximumApiLimit)> GetRestApiRateLimitDataFromHeaders() 76 | { 77 | HttpResponseMessage restApiResponse = await _client.GetAsync($"{GitHubConstants.GitHubRestApiUrl}/repos/brminnick/GitHubApiStatus"); 78 | restApiResponse.EnsureSuccessStatusCode(); 79 | 80 | TimeSpan rateLimitTimeRemaining = _gitHubApiStatusService.GetRateLimitTimeRemaining(restApiResponse.Headers); 81 | 82 | int rateLimit = _gitHubApiStatusService.GetRateLimit(restApiResponse.Headers); 83 | int remainingRequestCount = _gitHubApiStatusService.GetRemainingRequestCount(restApiResponse.Headers); 84 | 85 | bool isResponseFromAuthenticatedRequest = _gitHubApiStatusService.IsResponseFromAuthenticatedRequest(restApiResponse.Headers); 86 | 87 | bool hasReachedMaximumApiLimit = _gitHubApiStatusService.HasReachedMaximumApiCallLimit(restApiResponse.Headers); 88 | 89 | return (rateLimitTimeRemaining, rateLimit, remainingRequestCount, isResponseFromAuthenticatedRequest, hasReachedMaximumApiLimit); 90 | } 91 | 92 | static Task GetApiRateLimits() 93 | { 94 | if (string.IsNullOrWhiteSpace(GitHubConstants.PersonalAccessToken)) 95 | throw new ArgumentException("GitHubConstants.PersonalAccessToken Cannot be Empty"); 96 | 97 | return _gitHubApiStatusService.GetApiRateLimits(); 98 | } 99 | 100 | static HttpClient CreateGitHubHttpClient(in AuthenticationHeaderValue authenticationHeaderValue, in ProductHeaderValue productHeaderValue) 101 | { 102 | var client = new HttpClient(); 103 | client.DefaultRequestHeaders.Authorization = authenticationHeaderValue; 104 | client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(productHeaderValue)); 105 | 106 | return client; 107 | } 108 | } -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/App.cs: -------------------------------------------------------------------------------- 1 | namespace GitStatus; 2 | 3 | partial class App(AppShell appShell) : Application 4 | { 5 | readonly AppShell _appShell = appShell; 6 | 7 | protected override Window CreateWindow(IActivationState? activationState) 8 | { 9 | base.CreateWindow(activationState); 10 | return new(_appShell); 11 | } 12 | } -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/AppShell.cs: -------------------------------------------------------------------------------- 1 | namespace GitStatus; 2 | 3 | partial class AppShell : Shell 4 | { 5 | public AppShell(GraphQLApiStatusPage graphQLApiStatusPage, RestApiStatusPage restApiStatusPage) 6 | { 7 | Items.Add(new TabBar 8 | { 9 | Items = 10 | { 11 | graphQLApiStatusPage, 12 | restApiStatusPage 13 | } 14 | }); 15 | } 16 | } -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/GitStatus.Mobile.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | $(LatestSupportedTFM)-android;$(LatestSupportedTFM)-ios;$(LatestSupportedTFM)-maccatalyst 4 | $(TargetFrameworks);$(LatestSupportedTFM)-windows10.0.19041.0 5 | 6 | 7 | Exe 8 | GitStatus 9 | true 10 | true 11 | enable 12 | 13 | 14 | GitStatus 15 | 16 | 17 | com.companyname.gitstatus 18 | d59c13f1-7219-4005-84db-ecb85268429c 19 | 20 | 21 | 1.0 22 | 1 23 | 24 | 15.0 25 | 15.0 26 | 21.0 27 | 10.0.17763.0 28 | 10.0.17763.0 29 | 6.5 30 | 31 | true 32 | 33 | 34 | CsWinRT1028 35 | 36 | 37 | 38 | false 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/MauiProgram.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using CommunityToolkit.Maui; 3 | using CommunityToolkit.Maui.Markup; 4 | using GitHubApiStatus.Extensions; 5 | using GitStatus.Common; 6 | 7 | namespace GitStatus; 8 | 9 | public static class MauiProgram 10 | { 11 | public static MauiApp CreateMauiApp() 12 | { 13 | var builder = MauiApp.CreateBuilder() 14 | .UseMauiApp() 15 | .UseMauiCommunityToolkit() 16 | .UseMauiCommunityToolkitMarkup() 17 | .ConfigureFonts(fonts => 18 | { 19 | fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); 20 | fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); 21 | }); 22 | 23 | // Add Pages + ViewModels 24 | builder.Services.AddTransient(); 25 | builder.Services.AddTransientWithShellRoute(); 26 | builder.Services.AddTransientWithShellRoute(); 27 | 28 | // Add Services 29 | builder.Services.AddGitHubApiStatusService(new AuthenticationHeaderValue(GitHubConstants.AuthScheme, GitHubConstants.PersonalAccessToken), new ProductHeaderValue(nameof(GitStatus))); 30 | 31 | return builder.Build(); 32 | } 33 | 34 | static IServiceCollection AddTransientWithShellRoute(this IServiceCollection services) 35 | where TPage : BaseContentPage 36 | where TViewModel : BaseViewModel 37 | { 38 | return services.AddTransientWithShellRoute($"//{typeof(TPage).Name}"); 39 | } 40 | } -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/Pages/Base/BaseContentPage.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Maui.Controls.PlatformConfiguration; 2 | using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific; 3 | 4 | namespace GitStatus; 5 | 6 | abstract partial class BaseContentPage : ContentPage 7 | { 8 | protected BaseContentPage(in string title = "", bool shouldUseSafeArea = true) 9 | { 10 | Title = title; 11 | 12 | On().SetUseSafeArea(shouldUseSafeArea); 13 | On().SetModalPresentationStyle(UIModalPresentationStyle.FormSheet); 14 | } 15 | } 16 | 17 | abstract class BaseContentPage : BaseContentPage where T : BaseViewModel 18 | { 19 | protected BaseContentPage(in T viewModel, in string title, bool shouldUseSafeArea = false) 20 | : base(title, shouldUseSafeArea) 21 | { 22 | base.BindingContext = viewModel; 23 | } 24 | 25 | protected new T BindingContext => (T)base.BindingContext; 26 | } -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/Pages/Base/BaseStatusPage.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Maui.Markup; 2 | 3 | namespace GitStatus; 4 | 5 | abstract partial class BaseStatusPage : BaseContentPage where T : BaseStatusViewModel 6 | { 7 | protected BaseStatusPage(T statusViewModel, string title) : base(statusViewModel, title) 8 | { 9 | BackgroundColor = Colors.White; 10 | 11 | Content = new StackLayout 12 | { 13 | Children = 14 | { 15 | new Label() 16 | .TextColor(Colors.Black).Center().TextCenter() 17 | .Bind(Label.TextProperty, static (BaseStatusViewModel vm) => vm.StatusLabelText), 18 | 19 | new Button() 20 | .Text("Get Status").Center() 21 | .Bind(Button.CommandProperty, static (BaseStatusViewModel vm) => vm.GetStatusCommand, mode: BindingMode.OneTime), 22 | 23 | new ActivityIndicator { Color = Colors.Black }.Center() 24 | .Bind(IsVisibleProperty, static (BaseStatusViewModel vm) => vm.IsBusy) 25 | .Bind(ActivityIndicator.IsRunningProperty, static (BaseStatusViewModel vm) => vm.IsBusy) 26 | } 27 | }.Center(); 28 | } 29 | } -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/Pages/GraphQLApiStatusPage.cs: -------------------------------------------------------------------------------- 1 | namespace GitStatus; 2 | 3 | partial class GraphQLApiStatusPage : BaseStatusPage 4 | { 5 | public GraphQLApiStatusPage(GraphQLApiStatusViewModel graphQLApiStatusViewModel) : base(graphQLApiStatusViewModel, "GraphQL API Status") 6 | { 7 | } 8 | } -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/Pages/RestApiStatusPage.cs: -------------------------------------------------------------------------------- 1 | namespace GitStatus; 2 | 3 | partial class RestApiStatusPage : BaseStatusPage 4 | { 5 | public RestApiStatusPage(RestApiStatusViewModel restApiStatusViewModel) : base(restApiStatusViewModel, "REST API Status") 6 | { 7 | } 8 | } -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/Platforms/Android/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/Platforms/Android/MainActivity.cs: -------------------------------------------------------------------------------- 1 | using Android.App; 2 | using Android.Content.PM; 3 | 4 | namespace GitStatus; 5 | 6 | [Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] 7 | public class MainActivity : MauiAppCompatActivity 8 | { 9 | } -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/Platforms/Android/MainApplication.cs: -------------------------------------------------------------------------------- 1 | using Android.App; 2 | using Android.Runtime; 3 | 4 | namespace GitStatus; 5 | 6 | [Application] 7 | public class MainApplication : MauiApplication 8 | { 9 | public MainApplication(IntPtr handle, JniHandleOwnership ownership) 10 | : base(handle, ownership) 11 | { 12 | } 13 | 14 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 15 | } -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/Platforms/Android/Resources/values/colors.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | #512BD4 4 | #2B0B98 5 | #2B0B98 6 | 7 | 8 | -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/Platforms/MacCatalyst/AppDelegate.cs: -------------------------------------------------------------------------------- 1 | using Foundation; 2 | 3 | namespace GitStatus; 4 | 5 | [Register(nameof(AppDelegate))] 6 | public class AppDelegate : MauiUIApplicationDelegate 7 | { 8 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 9 | } -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/Platforms/MacCatalyst/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIDeviceFamily 6 | 7 | 1 8 | 2 9 | 10 | UIRequiredDeviceCapabilities 11 | 12 | arm64 13 | 14 | UISupportedInterfaceOrientations 15 | 16 | UIInterfaceOrientationPortrait 17 | UIInterfaceOrientationLandscapeLeft 18 | UIInterfaceOrientationLandscapeRight 19 | 20 | UISupportedInterfaceOrientations~ipad 21 | 22 | UIInterfaceOrientationPortrait 23 | UIInterfaceOrientationPortraitUpsideDown 24 | UIInterfaceOrientationLandscapeLeft 25 | UIInterfaceOrientationLandscapeRight 26 | 27 | XSAppIconAssets 28 | Assets.xcassets/appicon.appiconset 29 | 30 | 31 | -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/Platforms/MacCatalyst/Program.cs: -------------------------------------------------------------------------------- 1 | using UIKit; 2 | 3 | namespace GitStatus; 4 | 5 | public class Program 6 | { 7 | static void Main(string[] args) => UIApplication.Main(args, null, typeof(AppDelegate)); 8 | } -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/Platforms/Tizen/Main.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Maui; 3 | using Microsoft.Maui.Hosting; 4 | 5 | namespace GitStatus; 6 | 7 | class Program : MauiApplication 8 | { 9 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 10 | 11 | static void Main(string[] args) 12 | { 13 | var app = new Program(); 14 | app.Run(args); 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/Platforms/Tizen/tizen-manifest.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | maui-appicon-placeholder 7 | 8 | 9 | 10 | 11 | http://tizen.org/privilege/internet 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/Platforms/Windows/App.xaml: -------------------------------------------------------------------------------- 1 |  7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/Platforms/Windows/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.UI.Xaml; 2 | 3 | // To learn more about WinUI, the WinUI project structure, 4 | // and more about our project templates, see: http://aka.ms/winui-project-info. 5 | 6 | namespace GitStatus.WinUI; 7 | 8 | /// 9 | /// Provides application-specific behavior to supplement the default Application class. 10 | /// 11 | public partial class App : MauiWinUIApplication 12 | { 13 | /// 14 | /// Initializes the singleton application object. This is the first line of authored code 15 | /// executed, and as such is the logical equivalent of main() or WinMain(). 16 | /// 17 | public App() 18 | { 19 | this.InitializeComponent(); 20 | } 21 | 22 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 23 | } -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/Platforms/Windows/Package.appxmanifest: -------------------------------------------------------------------------------- 1 |  2 | 7 | 8 | 9 | 10 | 11 | $placeholder$ 12 | User Name 13 | $placeholder$.png 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/Platforms/Windows/app.manifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 11 | true/PM 12 | PerMonitorV2, PerMonitor 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/Platforms/iOS/AppDelegate.cs: -------------------------------------------------------------------------------- 1 | using Foundation; 2 | 3 | namespace GitStatus; 4 | 5 | [Register(nameof(AppDelegate))] 6 | public class AppDelegate : MauiUIApplicationDelegate 7 | { 8 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 9 | } -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/Platforms/iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LSRequiresIPhoneOS 6 | 7 | UIDeviceFamily 8 | 9 | 1 10 | 2 11 | 12 | UIRequiredDeviceCapabilities 13 | 14 | arm64 15 | 16 | UISupportedInterfaceOrientations 17 | 18 | UIInterfaceOrientationPortrait 19 | UIInterfaceOrientationLandscapeLeft 20 | UIInterfaceOrientationLandscapeRight 21 | 22 | UISupportedInterfaceOrientations~ipad 23 | 24 | UIInterfaceOrientationPortrait 25 | UIInterfaceOrientationPortraitUpsideDown 26 | UIInterfaceOrientationLandscapeLeft 27 | UIInterfaceOrientationLandscapeRight 28 | 29 | XSAppIconAssets 30 | Assets.xcassets/appicon.appiconset 31 | 32 | 33 | -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/Platforms/iOS/Program.cs: -------------------------------------------------------------------------------- 1 | using UIKit; 2 | 3 | namespace GitStatus; 4 | 5 | public class Program 6 | { 7 | static void Main(string[] args) => UIApplication.Main(args, null, typeof(AppDelegate)); 8 | } -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/Resources/AppIcon/appicon.svg: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/Resources/AppIcon/appiconfg.svg: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/Resources/Fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeTraveler/GitHubApiStatus/26c5ae40ca86fabc02000ad9d569d059b66cedc2/samples/GitStatus.Mobile/Resources/Fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/Resources/Fonts/OpenSans-Semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeTraveler/GitHubApiStatus/26c5ae40ca86fabc02000ad9d569d059b66cedc2/samples/GitStatus.Mobile/Resources/Fonts/OpenSans-Semibold.ttf -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/Resources/Raw/AboutAssets.txt: -------------------------------------------------------------------------------- 1 | Any raw assets you want to be deployed with your application can be placed in 2 | this directory (and child directories). Deployment of the asset to your application 3 | is automatically handled by the following `MauiAsset` Build Action within your `.csproj`. 4 | 5 | 6 | 7 | These files will be deployed with you package and will be accessible using Essentials: 8 | 9 | async Task LoadMauiAsset() 10 | { 11 | using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt"); 12 | using var reader = new StreamReader(stream); 13 | 14 | var contents = reader.ReadToEnd(); 15 | } 16 | 17 | 18 | -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/Resources/Splash/splash.svg: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/Resources/Styles/Colors.xaml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 6 | 7 | #512BD4 8 | #DFD8F7 9 | #2B0B98 10 | White 11 | Black 12 | #E1E1E1 13 | #C8C8C8 14 | #ACACAC 15 | #919191 16 | #6E6E6E 17 | #404040 18 | #212121 19 | #141414 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | #F7B548 35 | #FFD590 36 | #FFE5B9 37 | #28C2D1 38 | #7BDDEF 39 | #C3F2F4 40 | #3E8EED 41 | #72ACF1 42 | #A7CBF6 43 | 44 | 45 | -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/ViewModels/Base/BaseStatusViewModel.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | using CommunityToolkit.Mvvm.Input; 3 | 4 | namespace GitStatus; 5 | 6 | abstract partial class BaseStatusViewModel : BaseViewModel 7 | { 8 | protected BaseStatusViewModel() 9 | { 10 | StatusLabelText = string.Empty; 11 | } 12 | 13 | [ObservableProperty] 14 | public partial string StatusLabelText { get; protected set; } 15 | 16 | [ObservableProperty, NotifyPropertyChangedFor(nameof(IsNotBusy)), NotifyCanExecuteChangedFor(nameof(GetStatusCommand))] 17 | public partial bool IsBusy { get; protected set; } 18 | 19 | public bool IsNotBusy => !IsBusy; 20 | 21 | [RelayCommand(CanExecute = nameof(IsNotBusy))] 22 | protected abstract Task GetStatus(); 23 | } -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/ViewModels/Base/BaseViewModel.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | 3 | namespace GitStatus; 4 | 5 | abstract partial class BaseViewModel : ObservableObject 6 | { 7 | } -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/ViewModels/GraphQLApiStatusViewModel.cs: -------------------------------------------------------------------------------- 1 | using GitHubApiStatus; 2 | 3 | namespace GitStatus; 4 | 5 | partial class GraphQLApiStatusViewModel(IGitHubApiStatusService gitHubApiStatusService) : BaseStatusViewModel 6 | { 7 | readonly IGitHubApiStatusService _gitHubApiStatusService = gitHubApiStatusService; 8 | 9 | protected override async Task GetStatus() 10 | { 11 | var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5)); 12 | var apiRateLimitStatuses = await _gitHubApiStatusService.GetApiRateLimits(cancellationTokenSource.Token).ConfigureAwait(false); 13 | 14 | StatusLabelText = apiRateLimitStatuses.GraphQLApi.ToString(); 15 | } 16 | } -------------------------------------------------------------------------------- /samples/GitStatus.Mobile/ViewModels/RestApiStatusViewModel.cs: -------------------------------------------------------------------------------- 1 | using GitHubApiStatus; 2 | 3 | namespace GitStatus; 4 | 5 | partial class RestApiStatusViewModel(IGitHubApiStatusService gitHubApiStatusService) : BaseStatusViewModel 6 | { 7 | readonly IGitHubApiStatusService _gitHubApiStatusService = gitHubApiStatusService; 8 | 9 | protected override async Task GetStatus() 10 | { 11 | var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5)); 12 | var apiRateLimitStatuses = await _gitHubApiStatusService.GetApiRateLimits(cancellationTokenSource.Token).ConfigureAwait(false); 13 | 14 | StatusLabelText = apiRateLimitStatuses.RestApi.ToString(); 15 | } 16 | } -------------------------------------------------------------------------------- /samples/GitStatus.Web/App.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Sorry, there's nothing at this address.

8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /samples/GitStatus.Web/GitStatus.Web.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(LatestSupportedTFM) 5 | service-worker-assets.js 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /samples/GitStatus.Web/Pages/GraphQL.razor: -------------------------------------------------------------------------------- 1 | @page "/graphql" 2 | @using GitHubApiStatus 3 | @inject IGitHubApiStatusService GitHubApiStatusService 4 | 5 |

GitHub REST Api Status

6 | 7 |

@_graphQLApiStatus

8 | 9 | 10 | 11 | @code { 12 | string _graphQLApiStatus = string.Empty; 13 | 14 | async Task GetGraphQLApiStatus() 15 | { 16 | var apiRateLimitStatuses = await GitHubApiStatusService.GetApiRateLimits(System.Threading.CancellationToken.None).ConfigureAwait(false); 17 | _graphQLApiStatus = apiRateLimitStatuses.GraphQLApi.ToString(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /samples/GitStatus.Web/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 |

GitStatus

4 | 5 | 👈 Choose the GitHub API to verify 6 | -------------------------------------------------------------------------------- /samples/GitStatus.Web/Pages/Rest.razor: -------------------------------------------------------------------------------- 1 | @page "/rest" 2 | @using GitHubApiStatus 3 | @inject IGitHubApiStatusService GitHubApiStatusService 4 | 5 |

GitHub REST Api Status

6 | 7 |

@_restApiStatus

8 | 9 | 10 | 11 | @code { 12 | string _restApiStatus = string.Empty; 13 | 14 | async Task GetRestApiStatus() 15 | { 16 | var apiRateLimitStatuses = await GitHubApiStatusService.GetApiRateLimits(System.Threading.CancellationToken.None).ConfigureAwait(false); 17 | _restApiStatus = apiRateLimitStatuses.RestApi.ToString(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /samples/GitStatus.Web/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using GitHubApiStatus.Extensions; 3 | using GitStatus.Common; 4 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 5 | 6 | namespace GitStatus.Web; 7 | 8 | public class Program 9 | { 10 | public static Task Main(string[] args) 11 | { 12 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 13 | builder.RootComponents.Add("#app"); 14 | builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); 15 | 16 | builder.Services.AddGitHubApiStatusService(new AuthenticationHeaderValue(GitHubConstants.AuthScheme, GitHubConstants.PersonalAccessToken), new ProductHeaderValue(nameof(GitStatus))); 17 | 18 | return builder.Build().RunAsync(); 19 | } 20 | } -------------------------------------------------------------------------------- /samples/GitStatus.Web/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 |
4 | 7 | 8 |
9 |
10 | About 11 |
12 | 13 |
14 | @Body 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /samples/GitStatus.Web/Shared/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #f7f7f7; 17 | border-bottom: 1px solid #d6d5d5; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | } 28 | 29 | .top-row a:first-child { 30 | overflow: hidden; 31 | text-overflow: ellipsis; 32 | } 33 | 34 | @media (max-width: 640.98px) { 35 | .top-row:not(.auth) { 36 | display: none; 37 | } 38 | 39 | .top-row.auth { 40 | justify-content: space-between; 41 | } 42 | 43 | .top-row a, .top-row .btn-link { 44 | margin-left: 0; 45 | } 46 | } 47 | 48 | @media (min-width: 641px) { 49 | .page { 50 | flex-direction: row; 51 | } 52 | 53 | .sidebar { 54 | width: 250px; 55 | height: 100vh; 56 | position: sticky; 57 | top: 0; 58 | } 59 | 60 | .top-row { 61 | position: sticky; 62 | top: 0; 63 | z-index: 1; 64 | } 65 | 66 | .main > div { 67 | padding-left: 2rem !important; 68 | padding-right: 1.5rem !important; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /samples/GitStatus.Web/Shared/NavMenu.razor: -------------------------------------------------------------------------------- 1 |  7 | 8 |
9 | 26 |
27 | 28 | @code { 29 | private bool collapseNavMenu = true; 30 | 31 | private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null; 32 | 33 | private void ToggleNavMenu() 34 | { 35 | collapseNavMenu = !collapseNavMenu; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /samples/GitStatus.Web/Shared/NavMenu.razor.css: -------------------------------------------------------------------------------- 1 | .navbar-toggler { 2 | background-color: rgba(255, 255, 255, 0.1); 3 | } 4 | 5 | .top-row { 6 | height: 3.5rem; 7 | background-color: rgba(0,0,0,0.4); 8 | } 9 | 10 | .navbar-brand { 11 | font-size: 1.1rem; 12 | } 13 | 14 | .oi { 15 | width: 2rem; 16 | font-size: 1.1rem; 17 | vertical-align: text-top; 18 | top: -2px; 19 | } 20 | 21 | .nav-item { 22 | font-size: 0.9rem; 23 | padding-bottom: 0.5rem; 24 | } 25 | 26 | .nav-item:first-of-type { 27 | padding-top: 1rem; 28 | } 29 | 30 | .nav-item:last-of-type { 31 | padding-bottom: 1rem; 32 | } 33 | 34 | .nav-item ::deep a { 35 | color: #d7d7d7; 36 | border-radius: 4px; 37 | height: 3rem; 38 | display: flex; 39 | align-items: center; 40 | line-height: 3rem; 41 | } 42 | 43 | .nav-item ::deep a.active { 44 | background-color: rgba(255,255,255,0.25); 45 | color: white; 46 | } 47 | 48 | .nav-item ::deep a:hover { 49 | background-color: rgba(255,255,255,0.1); 50 | color: white; 51 | } 52 | 53 | @media (min-width: 641px) { 54 | .navbar-toggler { 55 | display: none; 56 | } 57 | 58 | .collapse { 59 | /* Never collapse the sidebar for wide screens */ 60 | display: block; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /samples/GitStatus.Web/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using Microsoft.AspNetCore.Components.Web.Virtualization 7 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 8 | @using Microsoft.JSInterop 9 | @using GitStatus.Web 10 | @using GitStatus.Web.Shared 11 | -------------------------------------------------------------------------------- /samples/GitStatus.Web/wwwroot/css/app.css: -------------------------------------------------------------------------------- 1 | @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); 2 | 3 | html, body { 4 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 5 | } 6 | 7 | a, .btn-link { 8 | color: #0366d6; 9 | } 10 | 11 | .btn-primary { 12 | color: #fff; 13 | background-color: #1b6ec2; 14 | border-color: #1861ac; 15 | } 16 | 17 | .content { 18 | padding-top: 1.1rem; 19 | } 20 | 21 | .valid.modified:not([type=checkbox]) { 22 | outline: 1px solid #26b050; 23 | } 24 | 25 | .invalid { 26 | outline: 1px solid red; 27 | } 28 | 29 | .validation-message { 30 | color: red; 31 | } 32 | 33 | #blazor-error-ui { 34 | background: lightyellow; 35 | bottom: 0; 36 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 37 | display: none; 38 | left: 0; 39 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 40 | position: fixed; 41 | width: 100%; 42 | z-index: 1000; 43 | } 44 | 45 | #blazor-error-ui .dismiss { 46 | cursor: pointer; 47 | position: absolute; 48 | right: 0.75rem; 49 | top: 0.5rem; 50 | } 51 | -------------------------------------------------------------------------------- /samples/GitStatus.Web/wwwroot/css/open-iconic/FONT-LICENSE: -------------------------------------------------------------------------------- 1 | SIL OPEN FONT LICENSE Version 1.1 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | PREAMBLE 6 | The goals of the Open Font License (OFL) are to stimulate worldwide 7 | development of collaborative font projects, to support the font creation 8 | efforts of academic and linguistic communities, and to provide a free and 9 | open framework in which fonts may be shared and improved in partnership 10 | with others. 11 | 12 | The OFL allows the licensed fonts to be used, studied, modified and 13 | redistributed freely as long as they are not sold by themselves. The 14 | fonts, including any derivative works, can be bundled, embedded, 15 | redistributed and/or sold with any software provided that any reserved 16 | names are not used by derivative works. The fonts and derivatives, 17 | however, cannot be released under any other type of license. The 18 | requirement for fonts to remain under this license does not apply 19 | to any document created using the fonts or their derivatives. 20 | 21 | DEFINITIONS 22 | "Font Software" refers to the set of files released by the Copyright 23 | Holder(s) under this license and clearly marked as such. This may 24 | include source files, build scripts and documentation. 25 | 26 | "Reserved Font Name" refers to any names specified as such after the 27 | copyright statement(s). 28 | 29 | "Original Version" refers to the collection of Font Software components as 30 | distributed by the Copyright Holder(s). 31 | 32 | "Modified Version" refers to any derivative made by adding to, deleting, 33 | or substituting -- in part or in whole -- any of the components of the 34 | Original Version, by changing formats or by porting the Font Software to a 35 | new environment. 36 | 37 | "Author" refers to any designer, engineer, programmer, technical 38 | writer or other person who contributed to the Font Software. 39 | 40 | PERMISSION & CONDITIONS 41 | Permission is hereby granted, free of charge, to any person obtaining 42 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 43 | redistribute, and sell modified and unmodified copies of the Font 44 | Software, subject to the following conditions: 45 | 46 | 1) Neither the Font Software nor any of its individual components, 47 | in Original or Modified Versions, may be sold by itself. 48 | 49 | 2) Original or Modified Versions of the Font Software may be bundled, 50 | redistributed and/or sold with any software, provided that each copy 51 | contains the above copyright notice and this license. These can be 52 | included either as stand-alone text files, human-readable headers or 53 | in the appropriate machine-readable metadata fields within text or 54 | binary files as long as those fields can be easily viewed by the user. 55 | 56 | 3) No Modified Version of the Font Software may use the Reserved Font 57 | Name(s) unless explicit written permission is granted by the corresponding 58 | Copyright Holder. This restriction only applies to the primary font name as 59 | presented to the users. 60 | 61 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 62 | Software shall not be used to promote, endorse or advertise any 63 | Modified Version, except to acknowledge the contribution(s) of the 64 | Copyright Holder(s) and the Author(s) or with their explicit written 65 | permission. 66 | 67 | 5) The Font Software, modified or unmodified, in part or in whole, 68 | must be distributed entirely under this license, and must not be 69 | distributed under any other license. The requirement for fonts to 70 | remain under this license does not apply to any document created 71 | using the Font Software. 72 | 73 | TERMINATION 74 | This license becomes null and void if any of the above conditions are 75 | not met. 76 | 77 | DISCLAIMER 78 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 79 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 80 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 81 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 82 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 83 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 84 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 85 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 86 | OTHER DEALINGS IN THE FONT SOFTWARE. 87 | -------------------------------------------------------------------------------- /samples/GitStatus.Web/wwwroot/css/open-iconic/ICON-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /samples/GitStatus.Web/wwwroot/css/open-iconic/README.md: -------------------------------------------------------------------------------- 1 | [Open Iconic v1.1.1](http://useiconic.com/open) 2 | =========== 3 | 4 | ### Open Iconic is the open source sibling of [Iconic](http://useiconic.com). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](http://useiconic.com/open#icons) 5 | 6 | 7 | 8 | ## What's in Open Iconic? 9 | 10 | * 223 icons designed to be legible down to 8 pixels 11 | * Super-light SVG files - 61.8 for the entire set 12 | * SVG sprite—the modern replacement for icon fonts 13 | * Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats 14 | * Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats 15 | * PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. 16 | 17 | 18 | ## Getting Started 19 | 20 | #### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](http://useiconic.com/open#icons) and [Reference](http://useiconic.com/open#reference) sections. 21 | 22 | ### General Usage 23 | 24 | #### Using Open Iconic's SVGs 25 | 26 | We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). 27 | 28 | ``` 29 | icon name 30 | ``` 31 | 32 | #### Using Open Iconic's SVG Sprite 33 | 34 | Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. 35 | 36 | Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* 37 | 38 | ``` 39 | 40 | 41 | 42 | ``` 43 | 44 | Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. 45 | 46 | ``` 47 | .icon { 48 | width: 16px; 49 | height: 16px; 50 | } 51 | ``` 52 | 53 | Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. 54 | 55 | ``` 56 | .icon-account-login { 57 | fill: #f00; 58 | } 59 | ``` 60 | 61 | To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). 62 | 63 | #### Using Open Iconic's Icon Font... 64 | 65 | 66 | ##### …with Bootstrap 67 | 68 | You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` 69 | 70 | 71 | ``` 72 | 73 | ``` 74 | 75 | 76 | ``` 77 | 78 | ``` 79 | 80 | ##### …with Foundation 81 | 82 | You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` 83 | 84 | ``` 85 | 86 | ``` 87 | 88 | 89 | ``` 90 | 91 | ``` 92 | 93 | ##### …on its own 94 | 95 | You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` 96 | 97 | ``` 98 | 99 | ``` 100 | 101 | ``` 102 | 103 | ``` 104 | 105 | 106 | ## License 107 | 108 | ### Icons 109 | 110 | All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). 111 | 112 | ### Fonts 113 | 114 | All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). 115 | -------------------------------------------------------------------------------- /samples/GitStatus.Web/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:Icons;src:url(../fonts/open-iconic.eot);src:url(../fonts/open-iconic.eot?#iconic-sm) format('embedded-opentype'),url(../fonts/open-iconic.woff) format('woff'),url(../fonts/open-iconic.ttf) format('truetype'),url(../fonts/open-iconic.otf) format('opentype'),url(../fonts/open-iconic.svg#iconic-sm) format('svg');font-weight:400;font-style:normal}.oi{position:relative;top:1px;display:inline-block;speak:none;font-family:Icons;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.oi:empty:before{width:1em;text-align:center;box-sizing:content-box}.oi.oi-align-center:before{text-align:center}.oi.oi-align-left:before{text-align:left}.oi.oi-align-right:before{text-align:right}.oi.oi-flip-horizontal:before{-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.oi.oi-flip-vertical:before{-webkit-transform:scale(1,-1);-ms-transform:scale(-1,1);transform:scale(1,-1)}.oi.oi-flip-horizontal-vertical:before{-webkit-transform:scale(-1,-1);-ms-transform:scale(-1,1);transform:scale(-1,-1)}.oi-account-login:before{content:'\e000'}.oi-account-logout:before{content:'\e001'}.oi-action-redo:before{content:'\e002'}.oi-action-undo:before{content:'\e003'}.oi-align-center:before{content:'\e004'}.oi-align-left:before{content:'\e005'}.oi-align-right:before{content:'\e006'}.oi-aperture:before{content:'\e007'}.oi-arrow-bottom:before{content:'\e008'}.oi-arrow-circle-bottom:before{content:'\e009'}.oi-arrow-circle-left:before{content:'\e00a'}.oi-arrow-circle-right:before{content:'\e00b'}.oi-arrow-circle-top:before{content:'\e00c'}.oi-arrow-left:before{content:'\e00d'}.oi-arrow-right:before{content:'\e00e'}.oi-arrow-thick-bottom:before{content:'\e00f'}.oi-arrow-thick-left:before{content:'\e010'}.oi-arrow-thick-right:before{content:'\e011'}.oi-arrow-thick-top:before{content:'\e012'}.oi-arrow-top:before{content:'\e013'}.oi-audio-spectrum:before{content:'\e014'}.oi-audio:before{content:'\e015'}.oi-badge:before{content:'\e016'}.oi-ban:before{content:'\e017'}.oi-bar-chart:before{content:'\e018'}.oi-basket:before{content:'\e019'}.oi-battery-empty:before{content:'\e01a'}.oi-battery-full:before{content:'\e01b'}.oi-beaker:before{content:'\e01c'}.oi-bell:before{content:'\e01d'}.oi-bluetooth:before{content:'\e01e'}.oi-bold:before{content:'\e01f'}.oi-bolt:before{content:'\e020'}.oi-book:before{content:'\e021'}.oi-bookmark:before{content:'\e022'}.oi-box:before{content:'\e023'}.oi-briefcase:before{content:'\e024'}.oi-british-pound:before{content:'\e025'}.oi-browser:before{content:'\e026'}.oi-brush:before{content:'\e027'}.oi-bug:before{content:'\e028'}.oi-bullhorn:before{content:'\e029'}.oi-calculator:before{content:'\e02a'}.oi-calendar:before{content:'\e02b'}.oi-camera-slr:before{content:'\e02c'}.oi-caret-bottom:before{content:'\e02d'}.oi-caret-left:before{content:'\e02e'}.oi-caret-right:before{content:'\e02f'}.oi-caret-top:before{content:'\e030'}.oi-cart:before{content:'\e031'}.oi-chat:before{content:'\e032'}.oi-check:before{content:'\e033'}.oi-chevron-bottom:before{content:'\e034'}.oi-chevron-left:before{content:'\e035'}.oi-chevron-right:before{content:'\e036'}.oi-chevron-top:before{content:'\e037'}.oi-circle-check:before{content:'\e038'}.oi-circle-x:before{content:'\e039'}.oi-clipboard:before{content:'\e03a'}.oi-clock:before{content:'\e03b'}.oi-cloud-download:before{content:'\e03c'}.oi-cloud-upload:before{content:'\e03d'}.oi-cloud:before{content:'\e03e'}.oi-cloudy:before{content:'\e03f'}.oi-code:before{content:'\e040'}.oi-cog:before{content:'\e041'}.oi-collapse-down:before{content:'\e042'}.oi-collapse-left:before{content:'\e043'}.oi-collapse-right:before{content:'\e044'}.oi-collapse-up:before{content:'\e045'}.oi-command:before{content:'\e046'}.oi-comment-square:before{content:'\e047'}.oi-compass:before{content:'\e048'}.oi-contrast:before{content:'\e049'}.oi-copywriting:before{content:'\e04a'}.oi-credit-card:before{content:'\e04b'}.oi-crop:before{content:'\e04c'}.oi-dashboard:before{content:'\e04d'}.oi-data-transfer-download:before{content:'\e04e'}.oi-data-transfer-upload:before{content:'\e04f'}.oi-delete:before{content:'\e050'}.oi-dial:before{content:'\e051'}.oi-document:before{content:'\e052'}.oi-dollar:before{content:'\e053'}.oi-double-quote-sans-left:before{content:'\e054'}.oi-double-quote-sans-right:before{content:'\e055'}.oi-double-quote-serif-left:before{content:'\e056'}.oi-double-quote-serif-right:before{content:'\e057'}.oi-droplet:before{content:'\e058'}.oi-eject:before{content:'\e059'}.oi-elevator:before{content:'\e05a'}.oi-ellipses:before{content:'\e05b'}.oi-envelope-closed:before{content:'\e05c'}.oi-envelope-open:before{content:'\e05d'}.oi-euro:before{content:'\e05e'}.oi-excerpt:before{content:'\e05f'}.oi-expand-down:before{content:'\e060'}.oi-expand-left:before{content:'\e061'}.oi-expand-right:before{content:'\e062'}.oi-expand-up:before{content:'\e063'}.oi-external-link:before{content:'\e064'}.oi-eye:before{content:'\e065'}.oi-eyedropper:before{content:'\e066'}.oi-file:before{content:'\e067'}.oi-fire:before{content:'\e068'}.oi-flag:before{content:'\e069'}.oi-flash:before{content:'\e06a'}.oi-folder:before{content:'\e06b'}.oi-fork:before{content:'\e06c'}.oi-fullscreen-enter:before{content:'\e06d'}.oi-fullscreen-exit:before{content:'\e06e'}.oi-globe:before{content:'\e06f'}.oi-graph:before{content:'\e070'}.oi-grid-four-up:before{content:'\e071'}.oi-grid-three-up:before{content:'\e072'}.oi-grid-two-up:before{content:'\e073'}.oi-hard-drive:before{content:'\e074'}.oi-header:before{content:'\e075'}.oi-headphones:before{content:'\e076'}.oi-heart:before{content:'\e077'}.oi-home:before{content:'\e078'}.oi-image:before{content:'\e079'}.oi-inbox:before{content:'\e07a'}.oi-infinity:before{content:'\e07b'}.oi-info:before{content:'\e07c'}.oi-italic:before{content:'\e07d'}.oi-justify-center:before{content:'\e07e'}.oi-justify-left:before{content:'\e07f'}.oi-justify-right:before{content:'\e080'}.oi-key:before{content:'\e081'}.oi-laptop:before{content:'\e082'}.oi-layers:before{content:'\e083'}.oi-lightbulb:before{content:'\e084'}.oi-link-broken:before{content:'\e085'}.oi-link-intact:before{content:'\e086'}.oi-list-rich:before{content:'\e087'}.oi-list:before{content:'\e088'}.oi-location:before{content:'\e089'}.oi-lock-locked:before{content:'\e08a'}.oi-lock-unlocked:before{content:'\e08b'}.oi-loop-circular:before{content:'\e08c'}.oi-loop-square:before{content:'\e08d'}.oi-loop:before{content:'\e08e'}.oi-magnifying-glass:before{content:'\e08f'}.oi-map-marker:before{content:'\e090'}.oi-map:before{content:'\e091'}.oi-media-pause:before{content:'\e092'}.oi-media-play:before{content:'\e093'}.oi-media-record:before{content:'\e094'}.oi-media-skip-backward:before{content:'\e095'}.oi-media-skip-forward:before{content:'\e096'}.oi-media-step-backward:before{content:'\e097'}.oi-media-step-forward:before{content:'\e098'}.oi-media-stop:before{content:'\e099'}.oi-medical-cross:before{content:'\e09a'}.oi-menu:before{content:'\e09b'}.oi-microphone:before{content:'\e09c'}.oi-minus:before{content:'\e09d'}.oi-monitor:before{content:'\e09e'}.oi-moon:before{content:'\e09f'}.oi-move:before{content:'\e0a0'}.oi-musical-note:before{content:'\e0a1'}.oi-paperclip:before{content:'\e0a2'}.oi-pencil:before{content:'\e0a3'}.oi-people:before{content:'\e0a4'}.oi-person:before{content:'\e0a5'}.oi-phone:before{content:'\e0a6'}.oi-pie-chart:before{content:'\e0a7'}.oi-pin:before{content:'\e0a8'}.oi-play-circle:before{content:'\e0a9'}.oi-plus:before{content:'\e0aa'}.oi-power-standby:before{content:'\e0ab'}.oi-print:before{content:'\e0ac'}.oi-project:before{content:'\e0ad'}.oi-pulse:before{content:'\e0ae'}.oi-puzzle-piece:before{content:'\e0af'}.oi-question-mark:before{content:'\e0b0'}.oi-rain:before{content:'\e0b1'}.oi-random:before{content:'\e0b2'}.oi-reload:before{content:'\e0b3'}.oi-resize-both:before{content:'\e0b4'}.oi-resize-height:before{content:'\e0b5'}.oi-resize-width:before{content:'\e0b6'}.oi-rss-alt:before{content:'\e0b7'}.oi-rss:before{content:'\e0b8'}.oi-script:before{content:'\e0b9'}.oi-share-boxed:before{content:'\e0ba'}.oi-share:before{content:'\e0bb'}.oi-shield:before{content:'\e0bc'}.oi-signal:before{content:'\e0bd'}.oi-signpost:before{content:'\e0be'}.oi-sort-ascending:before{content:'\e0bf'}.oi-sort-descending:before{content:'\e0c0'}.oi-spreadsheet:before{content:'\e0c1'}.oi-star:before{content:'\e0c2'}.oi-sun:before{content:'\e0c3'}.oi-tablet:before{content:'\e0c4'}.oi-tag:before{content:'\e0c5'}.oi-tags:before{content:'\e0c6'}.oi-target:before{content:'\e0c7'}.oi-task:before{content:'\e0c8'}.oi-terminal:before{content:'\e0c9'}.oi-text:before{content:'\e0ca'}.oi-thumb-down:before{content:'\e0cb'}.oi-thumb-up:before{content:'\e0cc'}.oi-timer:before{content:'\e0cd'}.oi-transfer:before{content:'\e0ce'}.oi-trash:before{content:'\e0cf'}.oi-underline:before{content:'\e0d0'}.oi-vertical-align-bottom:before{content:'\e0d1'}.oi-vertical-align-center:before{content:'\e0d2'}.oi-vertical-align-top:before{content:'\e0d3'}.oi-video:before{content:'\e0d4'}.oi-volume-high:before{content:'\e0d5'}.oi-volume-low:before{content:'\e0d6'}.oi-volume-off:before{content:'\e0d7'}.oi-warning:before{content:'\e0d8'}.oi-wifi:before{content:'\e0d9'}.oi-wrench:before{content:'\e0da'}.oi-x:before{content:'\e0db'}.oi-yen:before{content:'\e0dc'}.oi-zoom-in:before{content:'\e0dd'}.oi-zoom-out:before{content:'\e0de'} -------------------------------------------------------------------------------- /samples/GitStatus.Web/wwwroot/css/open-iconic/font/fonts/open-iconic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeTraveler/GitHubApiStatus/26c5ae40ca86fabc02000ad9d569d059b66cedc2/samples/GitStatus.Web/wwwroot/css/open-iconic/font/fonts/open-iconic.eot -------------------------------------------------------------------------------- /samples/GitStatus.Web/wwwroot/css/open-iconic/font/fonts/open-iconic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeTraveler/GitHubApiStatus/26c5ae40ca86fabc02000ad9d569d059b66cedc2/samples/GitStatus.Web/wwwroot/css/open-iconic/font/fonts/open-iconic.otf -------------------------------------------------------------------------------- /samples/GitStatus.Web/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeTraveler/GitHubApiStatus/26c5ae40ca86fabc02000ad9d569d059b66cedc2/samples/GitStatus.Web/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf -------------------------------------------------------------------------------- /samples/GitStatus.Web/wwwroot/css/open-iconic/font/fonts/open-iconic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeTraveler/GitHubApiStatus/26c5ae40ca86fabc02000ad9d569d059b66cedc2/samples/GitStatus.Web/wwwroot/css/open-iconic/font/fonts/open-iconic.woff -------------------------------------------------------------------------------- /samples/GitStatus.Web/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeTraveler/GitHubApiStatus/26c5ae40ca86fabc02000ad9d569d059b66cedc2/samples/GitStatus.Web/wwwroot/favicon.ico -------------------------------------------------------------------------------- /samples/GitStatus.Web/wwwroot/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeTraveler/GitHubApiStatus/26c5ae40ca86fabc02000ad9d569d059b66cedc2/samples/GitStatus.Web/wwwroot/icon-512.png -------------------------------------------------------------------------------- /samples/GitStatus.Web/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | GitStatus.Web 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
Loading...
18 | 19 |
20 | An unhandled error has occurred. 21 | Reload 22 | 🗙 23 |
24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /samples/GitStatus.Web/wwwroot/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GitStatus.Web", 3 | "short_name": "GitStatus.Web", 4 | "start_url": "./", 5 | "display": "standalone", 6 | "background_color": "#ffffff", 7 | "theme_color": "#03173d", 8 | "icons": [ 9 | { 10 | "src": "icon-512.png", 11 | "type": "image/png", 12 | "sizes": "512x512" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /samples/GitStatus.Web/wwwroot/sample-data/weather.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "date": "2018-05-06", 4 | "temperatureC": 1, 5 | "summary": "Freezing" 6 | }, 7 | { 8 | "date": "2018-05-07", 9 | "temperatureC": 14, 10 | "summary": "Bracing" 11 | }, 12 | { 13 | "date": "2018-05-08", 14 | "temperatureC": -13, 15 | "summary": "Freezing" 16 | }, 17 | { 18 | "date": "2018-05-09", 19 | "temperatureC": -16, 20 | "summary": "Balmy" 21 | }, 22 | { 23 | "date": "2018-05-10", 24 | "temperatureC": -2, 25 | "summary": "Chilly" 26 | } 27 | ] 28 | -------------------------------------------------------------------------------- /samples/GitStatus.Web/wwwroot/service-worker.js: -------------------------------------------------------------------------------- 1 | // In development, always fetch from the network and do not enable offline support. 2 | // This is because caching would make development more difficult (changes would not 3 | // be reflected on the first load after each change). 4 | self.addEventListener('fetch', () => { }); 5 | -------------------------------------------------------------------------------- /samples/GitStatus.Web/wwwroot/service-worker.published.js: -------------------------------------------------------------------------------- 1 | // Caution! Be sure you understand the caveats before publishing an application with 2 | // offline support. See https://aka.ms/blazor-offline-considerations 3 | 4 | self.importScripts('./service-worker-assets.js'); 5 | self.addEventListener('install', event => event.waitUntil(onInstall(event))); 6 | self.addEventListener('activate', event => event.waitUntil(onActivate(event))); 7 | self.addEventListener('fetch', event => event.respondWith(onFetch(event))); 8 | 9 | const cacheNamePrefix = 'offline-cache-'; 10 | const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`; 11 | const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/ ]; 12 | const offlineAssetsExclude = [ /^service-worker\.js$/ ]; 13 | 14 | async function onInstall(event) { 15 | console.info('Service worker: Install'); 16 | 17 | // Fetch and cache all matching items from the assets manifest 18 | const assetsRequests = self.assetsManifest.assets 19 | .filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url))) 20 | .filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url))) 21 | .map(asset => new Request(asset.url, { integrity: asset.hash })); 22 | await caches.open(cacheName).then(cache => cache.addAll(assetsRequests)); 23 | } 24 | 25 | async function onActivate(event) { 26 | console.info('Service worker: Activate'); 27 | 28 | // Delete unused caches 29 | const cacheKeys = await caches.keys(); 30 | await Promise.all(cacheKeys 31 | .filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName) 32 | .map(key => caches.delete(key))); 33 | } 34 | 35 | async function onFetch(event) { 36 | let cachedResponse = null; 37 | if (event.request.method === 'GET') { 38 | // For all navigation requests, try to serve index.html from cache 39 | // If you need some URLs to be server-rendered, edit the following check to exclude those URLs 40 | const shouldServeIndexHtml = event.request.mode === 'navigate'; 41 | 42 | const request = shouldServeIndexHtml ? 'index.html' : event.request; 43 | const cache = await caches.open(cacheName); 44 | cachedResponse = await cache.match(request); 45 | } 46 | 47 | return cachedResponse || fetch(event.request); 48 | } 49 | -------------------------------------------------------------------------------- /src/GitHubApiStatus.Extensions.UnitTests/GitHubApiStatus.Extensions.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net9.0 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/GitHubApiStatus.Extensions.UnitTests/GitHubApiStatusServiceExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using GitStatus.Common; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using NUnit.Framework; 5 | 6 | namespace GitHubApiStatus.Extensions.UnitTests; 7 | 8 | class GitHubApiStatusServiceExtensionsTests 9 | { 10 | [Test] 11 | public void NullProductHeaderValue() 12 | { 13 | //Arrange 14 | var services = new ServiceCollection(); 15 | ProductHeaderValue? productHeaderValue = null; 16 | var authenticationHeaderValue = GetAuthenticationHeaderValue(); 17 | 18 | //Act 19 | 20 | //Assert 21 | #pragma warning disable CS8604 // Possible null reference argument. 22 | Assert.That(() => services.AddGitHubApiStatusService(authenticationHeaderValue, productHeaderValue), Throws.TypeOf()); 23 | #pragma warning restore CS8604 // Possible null reference argument. 24 | } 25 | 26 | [Test] 27 | public void EmptyProductHeaderValue() 28 | { 29 | //Arrange 30 | 31 | //Act 32 | 33 | //Assert 34 | Assert.That(() => new ProductHeaderValue(""), Throws.TypeOf()); 35 | } 36 | 37 | [Test] 38 | public void WhiteSpaceProductHeaderValue() 39 | { 40 | //Arrange 41 | 42 | //Act 43 | 44 | //Assert 45 | #if NET9_0_OR_GREATER 46 | Assert.That(() => new ProductHeaderValue(" "), Throws.TypeOf()); 47 | #else 48 | Assert.That(() => new ProductHeaderValue(" "), Throws.TypeOf()); 49 | #endif 50 | } 51 | 52 | [Test] 53 | public void NullAuthenticationHeaderValue() 54 | { 55 | //Arrange 56 | var services = new ServiceCollection(); 57 | var productHeaderValue = GetProductHeaderValue(); 58 | AuthenticationHeaderValue? authenticationHeaderValue = null; 59 | 60 | //Act 61 | 62 | //Assert 63 | #pragma warning disable CS8604 // Possible null reference argument. 64 | Assert.That(() => services.AddGitHubApiStatusService(authenticationHeaderValue, productHeaderValue), Throws.TypeOf()); 65 | #pragma warning restore CS8604 // Possible null reference argument. 66 | } 67 | 68 | [TestCase("Basic")] 69 | [TestCase("Oauth")] 70 | [TestCase("Digest")] 71 | public void InvalidSchemeAuthenticationHeaderValue(string scheme) 72 | { 73 | //Arrange 74 | var services = new ServiceCollection(); 75 | var productHeaderValue = GetProductHeaderValue(); 76 | var authenticationHeaderValue = new AuthenticationHeaderValue(scheme, GitHubConstants.PersonalAccessToken); 77 | 78 | //Act 79 | 80 | //Assert 81 | Assert.That(() => services.AddGitHubApiStatusService(authenticationHeaderValue, productHeaderValue), Throws.TypeOf()); 82 | } 83 | 84 | [TestCase(null)] 85 | [TestCase("")] 86 | [TestCase(" ")] 87 | public void InvalidParameterAuthenticationHeaderValue(string? parameter) 88 | { 89 | //Arrange 90 | var services = new ServiceCollection(); 91 | var productHeaderValue = GetProductHeaderValue(); 92 | var authenticationHeaderValue = new AuthenticationHeaderValue(GitHubConstants.AuthScheme, parameter); 93 | 94 | //Act 95 | 96 | //Assert 97 | Assert.That(() => services.AddGitHubApiStatusService(authenticationHeaderValue, productHeaderValue), Throws.TypeOf()); 98 | } 99 | 100 | [Test] 101 | public async Task AddGitHubApiStatusService() 102 | { 103 | //Arrange 104 | var services = new ServiceCollection(); 105 | var productHeaderValue = GetProductHeaderValue(); 106 | var authenticationHeaderValue = GetAuthenticationHeaderValue(); 107 | 108 | services.AddGitHubApiStatusService(authenticationHeaderValue, productHeaderValue); 109 | 110 | var container = services.BuildServiceProvider(); 111 | 112 | //Act 113 | var gitHubApiStatusService = container.GetRequiredService(); 114 | 115 | var apiRateLimits = await gitHubApiStatusService.GetApiRateLimits(CancellationToken.None).ConfigureAwait(false); 116 | 117 | //Assert 118 | Assert.Multiple(() => 119 | { 120 | Assert.That(gitHubApiStatusService, Is.Not.Null); 121 | Assert.That(apiRateLimits, Is.Not.Null); 122 | Assert.That(apiRateLimits.AppManifestConfiguration, Is.Not.Null); 123 | Assert.That(apiRateLimits.CodeScanningUpload, Is.Not.Null); 124 | Assert.That(apiRateLimits.GraphQLApi, Is.Not.Null); 125 | Assert.That(apiRateLimits.RestApi, Is.Not.Null); 126 | Assert.That(apiRateLimits.SearchApi, Is.Not.Null); 127 | Assert.That(apiRateLimits.SourceImport, Is.Not.Null); 128 | }); 129 | } 130 | 131 | [Test] 132 | public async Task AddMockGitHubApiStatusService() 133 | { 134 | //Arrange 135 | var services = new ServiceCollection(); 136 | var productHeaderValue = GetProductHeaderValue(); 137 | var authenticationHeaderValue = GetAuthenticationHeaderValue(); 138 | 139 | services.AddGitHubApiStatusService(authenticationHeaderValue, productHeaderValue); 140 | 141 | var container = services.BuildServiceProvider(); 142 | 143 | //Act 144 | var gitHubApiStatusServce = container.GetRequiredService(); 145 | 146 | var apiRateLimits = await gitHubApiStatusServce.GetApiRateLimits(CancellationToken.None).ConfigureAwait(false); 147 | 148 | //Assert 149 | Assert.Multiple(() => 150 | { 151 | Assert.That(gitHubApiStatusServce, Is.Not.Null); 152 | Assert.That(apiRateLimits, Is.Not.Null); 153 | Assert.That(apiRateLimits.AppManifestConfiguration, Is.Not.Null); 154 | Assert.That(apiRateLimits.CodeScanningUpload, Is.Not.Null); 155 | Assert.That(apiRateLimits.GraphQLApi, Is.Not.Null); 156 | Assert.That(apiRateLimits.RestApi, Is.Not.Null); 157 | Assert.That(apiRateLimits.SearchApi, Is.Not.Null); 158 | Assert.That(apiRateLimits.SourceImport, Is.Not.Null); 159 | }); 160 | } 161 | 162 | static AuthenticationHeaderValue GetAuthenticationHeaderValue() => new(GitHubConstants.AuthScheme, GitHubConstants.PersonalAccessToken); 163 | static ProductHeaderValue GetProductHeaderValue() => new(nameof(GitHubApiStatus)); 164 | } 165 | -------------------------------------------------------------------------------- /src/GitHubApiStatus.Extensions.UnitTests/MockGitHubApiStatusService.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Net.Http.Headers; 3 | 4 | namespace GitHubApiStatus.Extensions; 5 | 6 | public sealed class MockGitHubApiStatusService(HttpClient httpClient) : IGitHubApiStatusService 7 | { 8 | public bool IsProductHeaderValueValid => true; 9 | public bool IsAuthenticationHeaderValueSet => true; 10 | 11 | public Task GetApiRateLimits(CancellationToken cancellationToken) 12 | { 13 | var apiStatus = new RateLimitStatus(5000, 5000, DateTimeOffset.UtcNow.AddMinutes(1).ToUnixTimeSeconds()); 14 | return Task.FromResult(new GitHubApiRateLimits(apiStatus, apiStatus, apiStatus, apiStatus, apiStatus, apiStatus)); 15 | } 16 | 17 | public int GetRateLimit(in HttpResponseHeaders httpResponseHeaders) => 5000; 18 | public int GetRemainingRequestCount(in HttpResponseHeaders httpResponseHeaders) => 5000; 19 | public bool HasReachedMaximumApiCallLimit(in HttpResponseHeaders httpResponseHeaders) => false; 20 | public bool IsResponseFromAuthenticatedRequest(in HttpResponseHeaders httpResponseHeaders) => true; 21 | public TimeSpan GetRateLimitTimeRemaining(in HttpResponseHeaders httpResponseHeaders) => new(1, 0, 0); 22 | public DateTimeOffset GetRateLimitResetDateTime(in HttpResponseHeaders httpResponseHeaders) => DateTimeOffset.UtcNow; 23 | public long GetRateLimitResetDateTime_UnixEpochSeconds(in HttpResponseHeaders httpResponseHeaders) => DateTimeOffset.UtcNow.AddMinutes(1).ToUnixTimeSeconds(); 24 | 25 | public void AddProductHeaderValue(ProductHeaderValue productHeaderValue) 26 | { 27 | 28 | } 29 | 30 | public void SetAuthenticationHeaderValue(AuthenticationHeaderValue authenticationHeaderValue) 31 | { 32 | 33 | } 34 | 35 | public bool IsAbuseRateLimit(in HttpResponseHeaders httpResponseHeaders, [NotNullWhen(true)] out TimeSpan? delta) 36 | { 37 | delta = null; 38 | return false; 39 | } 40 | 41 | public void Dispose() 42 | { 43 | httpClient.Dispose(); 44 | } 45 | } -------------------------------------------------------------------------------- /src/GitHubApiStatus.Extensions/.editorconfig: -------------------------------------------------------------------------------- 1 | root = false 2 | 3 | [*.cs] 4 | 5 | # Missing XML comment for publicly visible type or member 6 | dotnet_diagnostic.CS1591.severity = error 7 | 8 | # String comparison preferences 9 | dotnet_diagnostic.CA1307.severity = error 10 | dotnet_diagnostic.CA1309.severity = error -------------------------------------------------------------------------------- /src/GitHubApiStatus.Extensions/GitHubApiStatus.Extensions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0;netstandard2.1;net8.0;net9.0 4 | true 5 | GitHubApiStatus.Extensions.snk 6 | GitHubApiStatus.Extensions 7 | GitHubApiStatus.Extensions 8 | GitHubApiStatus.Extensions 9 | true 10 | true 11 | true 12 | snupkg 13 | 14 | Extentions for GitHub API Rate Limit Status 15 | - Implements Microsoft.Extensions.DependencyInjection 16 | - Implements Microsoft.Extensions.Http 17 | 18 | github, git, api, rate, rate limit 19 | Extensions for GitHub API Rate Limit Status 20 | 21 | Extentions for GitHub API Rate Limit Status 22 | - Implements Microsoft.Extensions.DependencyInjection 23 | - Implements Microsoft.Extensions.Http 24 | 25 | 26 | New In This Release: 27 | - Add .NET 9.0 28 | 29 | $(NuGetVersion) 30 | https://github.com/brminnick/GitHubApiStatus 31 | $(AssemblyName) ($(TargetFramework)) 32 | 1.0.0.0 33 | 1.0.0.0 34 | $(Version)$(VersionSuffix) 35 | Brandon Minnick 36 | Brandon Minnick 37 | en 38 | ©Copyright 2020 Brandon Minnick. All rights reserved. 39 | false 40 | $(DefineConstants); 41 | false 42 | false 43 | MIT 44 | https://github.com/brminnick/GitHubApiStatus 45 | portable 46 | Release;Debug 47 | false 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/GitHubApiStatus.Extensions/GitHubApiStatus.Extensions.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeTraveler/GitHubApiStatus/26c5ae40ca86fabc02000ad9d569d059b66cedc2/src/GitHubApiStatus.Extensions/GitHubApiStatus.Extensions.snk -------------------------------------------------------------------------------- /src/GitHubApiStatus.Extensions/GitHubApiStatusServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace GitHubApiStatus.Extensions; 5 | 6 | /// 7 | /// Extension Methods for GitHubApiStatusService 8 | /// 9 | public static class GitHubApiStatusServiceExtensions 10 | { 11 | /// 12 | /// Adds IGitHubApiStatusService to IServiceCollection 13 | /// 14 | /// Microsoft.Extensions.DependencyInjection.IServiceCollection 15 | /// GitHub API Personal Access Token 16 | /// UserAgent 17 | /// IHttpClientBuilder 18 | public static IHttpClientBuilder AddGitHubApiStatusService(this IServiceCollection services, AuthenticationHeaderValue authenticationHeaderValue, ProductHeaderValue productHeaderValue) => 19 | services.AddGitHubApiStatusService(authenticationHeaderValue, productHeaderValue); 20 | 21 | /// 22 | /// Adds IGitHubApiStatusService to IServiceCollection using custom implementation of IGitHubApiStatusService 23 | /// 24 | /// Implementation of IGitHubApiStatusService 25 | /// Microsoft.Extensions.DependencyInjection.IServiceCollection 26 | /// GitHub API Personal Access Token 27 | /// UserAgent 28 | /// IHttpClientBuilder 29 | public static IHttpClientBuilder AddGitHubApiStatusService(this IServiceCollection services, AuthenticationHeaderValue authenticationHeaderValue, ProductHeaderValue productHeaderValue) where TGitHubApiStatusService : class, IGitHubApiStatusService 30 | { 31 | if (productHeaderValue is null) 32 | throw new ArgumentNullException(nameof(productHeaderValue)); 33 | 34 | if (string.IsNullOrWhiteSpace(productHeaderValue?.Name)) 35 | throw new ArgumentException($"{nameof(ProductHeaderValue)}.{nameof(ProductHeaderValue.Name)} cannot be null or whitespace", nameof(productHeaderValue)); 36 | 37 | if (authenticationHeaderValue is null) 38 | throw new ArgumentNullException(nameof(authenticationHeaderValue)); 39 | 40 | if (!authenticationHeaderValue.Scheme.Equals("bearer", StringComparison.OrdinalIgnoreCase)) 41 | throw new ArgumentException($"{nameof(AuthenticationHeaderValue)}.{nameof(AuthenticationHeaderValue.Scheme)} must be `bearer`", nameof(authenticationHeaderValue)); 42 | 43 | if (string.IsNullOrWhiteSpace(authenticationHeaderValue.Parameter)) 44 | throw new ArgumentException($"{nameof(AuthenticationHeaderValue)}.{nameof(AuthenticationHeaderValue.Parameter)} cannot be blank", nameof(authenticationHeaderValue)); 45 | 46 | return services.AddHttpClient(client => 47 | { 48 | client.DefaultRequestHeaders.Authorization = authenticationHeaderValue; 49 | client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(productHeaderValue)); 50 | }); 51 | } 52 | } -------------------------------------------------------------------------------- /src/GitHubApiStatus.Extensions/README.md: -------------------------------------------------------------------------------- 1 | # GitHubApiStatus.Extensions 2 | 3 | [![NuGet](https://buildstats.info/nuget/GitHubApiStatus.Extensions?includePreReleases=true)](https://www.nuget.org/packages/GitHubApiStatus.Extensions/) 4 | 5 | - Available on NuGet: https://www.nuget.org/packages/GitHubApiStatus.Extensions/ 6 | - Add to any project supporting .NET Standard 2.0 7 | - Leverages [Microsoft.Extensions.Http](https://www.nuget.org/packages/Microsoft.Extensions.Http/) 8 | 9 | ## API 10 | 11 | ### AddGitHubApiStatusService 12 | 13 | ```csharp 14 | public static IHttpClientBuilder AddGitHubApiStatusService(this IServiceCollection services, AuthenticationHeaderValue authenticationHeaderValue, ProductHeaderValue productHeaderValue) 15 | ``` 16 | - Adds GitHubApiStatus.GitHubApiStatusService to `Microsoft.Extensions.DependencyInjection.IServiceCollection` 17 | 18 | ### AddGitHubApiStatusService<TGitHubApiStatusService> 19 | 20 | ```csharp 21 | public static IHttpClientBuilder AddGitHubApiStatusService(this IServiceCollection services, AuthenticationHeaderValue authenticationHeaderValue, ProductHeaderValue productHeaderValue) where TGitHubApiStatusService : IGitHubApiStatusService 22 | ``` 23 | - Adds a custom implementation of IGitHubApiStatusService to `Microsoft.Extensions.DependencyInjection.IServiceCollection` 24 | 25 | ## Dependency Injection 26 | 27 | - [Jump to Blazor Example](#blazor-example) 28 | - [Jump to ASP.NET Core Example](#aspnet-core-example) 29 | - [Jump to Azure Functions Example](#azure-functions-example) 30 | 31 | ### Blazor Example 32 | 33 | ```csharp 34 | public class Program 35 | { 36 | public static Task Main(string[] args) 37 | { 38 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 39 | builder.RootComponents.Add("#app"); 40 | builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); 41 | 42 | // AddGitHubApiStatusService 43 | builder.Services.AddGitHubApiStatusService(new AuthenticationHeaderValue("bearer", "[Your GitHub Personal Access Token, e.g. 123456789012345]"), new ProductHeaderValue("MyApp")) 44 | .ConfigurePrimaryHttpMessageHandler(config => new HttpClientHandler { AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate }); 45 | 46 | return builder.Build().RunAsync(); 47 | } 48 | } 49 | ``` 50 | 51 | ```csharp 52 | @page "/graphql" 53 | @using GitHubApiStatus 54 | @inject IGitHubApiStatusService GitHubApiStatusService 55 | 56 |

GitHub REST Api Status

57 | 58 |

@_graphQLApiStatus

59 | 60 | 61 | 62 | @code { 63 | string _graphQLApiStatus = string.Empty; 64 | 65 | async Task GetGraphQLApiStatus() 66 | { 67 | var apiRateLimitStatuses = await GitHubApiStatusService.GetApiRateLimits(System.Threading.CancellationToken.None).ConfigureAwait(false); 68 | _graphQLApiStatus = apiRateLimitStatuses.GraphQLApi.ToString(); 69 | } 70 | } 71 | ``` 72 | 73 | ### ASP.NET Core Example 74 | 75 | - Learn more about [Dependency Injection in ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-5.0&WT.mc_id=mobile-11370-bramin) 76 | 77 | ```csharp 78 | public class Startup 79 | { 80 | // ... 81 | 82 | public void ConfigureServices(IServiceCollection services) 83 | { 84 | services.AddGitHubApiStatusService(new AuthenticationHeaderValue("bearer", "[Your GitHub Personal Access Token, e.g. 123456789012345]"), new ProductHeaderValue("MyApp")) 85 | .ConfigurePrimaryHttpMessageHandler(config => new HttpClientHandler { AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate }); 86 | 87 | services.AddRazorPages(); 88 | } 89 | 90 | /// ... 91 | 92 | } 93 | ``` 94 | 95 | ```csharp 96 | class MyPageModel : PageModel 97 | { 98 | readonly ILogger _logger; 99 | readonly IGitHubApiStatusService _gitHubApiStatusService; 100 | 101 | public MyPageModel(IGitHubApiStatusService gitHubApiStatusService, ILogger logger) 102 | { 103 | _logger = logger; 104 | _gitHubApiStatusService = gitHubApiStatusService; 105 | } 106 | 107 | // ... 108 | } 109 | ``` 110 | 111 | ### Azure Functions Example 112 | 113 | - Requires [Microsoft.Azure.Functions.Extensions NuGet Package](https://www.nuget.org/packages/Microsoft.Azure.Functions.Extensions/) 114 | - Learn More about [Azure Functions Dependency Injection](https://docs.microsoft.com/azure/azure-functions/functions-dotnet-dependency-injection?WT.mc_id=mobile-11370-bramin) 115 | 116 | ```csharp 117 | [assembly: FunctionsStartup(typeof(MyApp.Functions.Startup))] 118 | namespace MyApp.Functions 119 | { 120 | public class Startup : FunctionsStartup 121 | { 122 | public override void Configure(IFunctionsHostBuilder builder) 123 | { 124 | builder.Services.AddGitHubApiStatusService(new AuthenticationHeaderValue("bearer", "[Your GitHub Personal Access Token, e.g. 123456789012345]"), new ProductHeaderValue("MyApp")) 125 | .ConfigurePrimaryHttpMessageHandler(config => new HttpClientHandler { AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate }); 126 | } 127 | } 128 | } 129 | ``` 130 | 131 | ```csharp 132 | class GitHubApiStatusFunction 133 | { 134 | readonly IGitHubApiStatusService _gitHubApiStatusService; 135 | 136 | public MyHttpTriggerFunction(IGitHubApiStatusService gitHubApiStatusService) => _gitHubApiStatusService = gitHubApiStatusService 137 | 138 | [FunctionName("GitHubApiStatus")] 139 | public async Task Run([HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req, ILogger log) 140 | { 141 | log.LogInformation("C# HTTP trigger function processed a request."); 142 | 143 | var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(2)); 144 | var apiStatus = await _gitHubApiStatusService.GetApiRateLimits(cancellationTokenSource.Token).ConfigureAwait(false); 145 | 146 | return new OkObjectResult(apiStatus); 147 | } 148 | } 149 | ``` 150 | -------------------------------------------------------------------------------- /src/GitHubApiStatus.UnitTests/GitHubApiStatus.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net9.0 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/GitHubApiStatus.UnitTests/Models/GraphQLError.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | 4 | namespace GitHubApiStatus.UnitTests; 5 | 6 | class GraphQLError 7 | { 8 | public GraphQLError(string message, GraphQLLocation[] locations) 9 | { 10 | Message = message; 11 | Locations = locations; 12 | } 13 | 14 | [JsonProperty("message")] 15 | public string Message { get; } 16 | 17 | [JsonProperty("locations")] 18 | public GraphQLLocation[] Locations { get; } 19 | 20 | [JsonExtensionData] 21 | public IDictionary? AdditonalEntries { get; set; } 22 | } 23 | 24 | class GraphQLLocation 25 | { 26 | [JsonProperty("line")] 27 | public long Line { get; } 28 | 29 | [JsonProperty("column")] 30 | public long Column { get; } 31 | } -------------------------------------------------------------------------------- /src/GitHubApiStatus.UnitTests/Models/GraphQLRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace GitHubApiStatus.UnitTests; 4 | 5 | class GraphQLRequest 6 | { 7 | public GraphQLRequest(string query, string variables = "") 8 | { 9 | Query = query; 10 | Variables = variables; 11 | } 12 | 13 | [JsonProperty("query")] 14 | public string Query { get; } 15 | 16 | [JsonProperty("variables")] 17 | public string Variables { get; } 18 | } -------------------------------------------------------------------------------- /src/GitHubApiStatus.UnitTests/Models/GraphQLResponse.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace GitHubApiStatus.UnitTests; 4 | 5 | class GraphQLResponse 6 | { 7 | public GraphQLResponse(T data, GraphQLError[] errors) 8 | { 9 | Data = data; 10 | Errors = errors; 11 | } 12 | 13 | [JsonProperty("data")] 14 | public T Data { get; } 15 | 16 | [JsonProperty("errors")] 17 | public GraphQLError[] Errors { get; } 18 | } -------------------------------------------------------------------------------- /src/GitHubApiStatus.UnitTests/Tests/Base/BaseTest.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Http; 3 | using System.Net.Http.Headers; 4 | using GitStatus.Common; 5 | using Newtonsoft.Json; 6 | using NUnit.Framework; 7 | 8 | namespace GitHubApiStatus.UnitTests; 9 | 10 | abstract class BaseTest 11 | { 12 | const string _authorizationHeaderKey = "Authorization"; 13 | 14 | HttpClient _client = CreateGitHubHttpClient(new AuthenticationHeaderValue(GitHubConstants.AuthScheme, GitHubConstants.PersonalAccessToken), new ProductHeaderValue(nameof(GitHubApiStatus))); 15 | 16 | protected IGitHubApiStatusService GitHubApiStatusService { get; private set; } = new GitHubApiStatusService(); 17 | 18 | [SetUp] 19 | protected virtual Task BeforeEachTest() 20 | { 21 | _client = CreateGitHubHttpClient(new AuthenticationHeaderValue(GitHubConstants.AuthScheme, GitHubConstants.PersonalAccessToken), new ProductHeaderValue(nameof(GitHubApiStatus))); 22 | GitHubApiStatusService = new GitHubApiStatusService(_client); 23 | return Task.CompletedTask; 24 | } 25 | 26 | [TearDown] 27 | protected virtual Task AfterEachTest() 28 | { 29 | GitHubApiStatusService.SetAuthenticationHeaderValue(new AuthenticationHeaderValue(GitHubConstants.AuthScheme, GitHubConstants.PersonalAccessToken)); 30 | _client.Dispose(); 31 | GitHubApiStatusService.Dispose(); 32 | return Task.CompletedTask; 33 | } 34 | 35 | [OneTimeTearDown] 36 | protected virtual Task AfterAllTests() 37 | { 38 | _client.Dispose(); 39 | GitHubApiStatusService.Dispose(); 40 | return Task.CompletedTask; 41 | } 42 | 43 | protected static HttpResponseHeaders CreateHttpResponseHeaders(in int rateLimit, in DateTimeOffset rateLimitResetTime, in int remainingRequestCount, in HttpStatusCode httpStatusCode = HttpStatusCode.OK, in bool isAuthenticated = true, in bool isAbuseRateLimit = false) 44 | { 45 | if (remainingRequestCount > rateLimit) 46 | throw new ArgumentOutOfRangeException(nameof(remainingRequestCount), $"{nameof(remainingRequestCount)} must be less than or equal to {nameof(rateLimit)}"); 47 | 48 | var httpResponse = new HttpResponseMessage(httpStatusCode) 49 | { 50 | Headers = 51 | { 52 | { GitHubApiStatus.GitHubApiStatusService.RateLimitHeader, rateLimit.ToString() }, 53 | { GitHubApiStatus.GitHubApiStatusService.RateLimitResetHeader, GetTimeInUnixEpochSeconds(rateLimitResetTime).ToString() }, 54 | { GitHubApiStatus.GitHubApiStatusService.RateLimitRemainingHeader, remainingRequestCount.ToString() } 55 | } 56 | }; 57 | 58 | if (isAbuseRateLimit) 59 | httpResponse.Headers.RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromMinutes(1)); 60 | 61 | if (isAuthenticated) 62 | httpResponse.Headers.Vary.Add(_authorizationHeaderKey); 63 | 64 | return httpResponse.Headers; 65 | } 66 | 67 | protected static HttpClient CreateGitHubHttpClient(in AuthenticationHeaderValue authenticationHeaderValue, in ProductHeaderValue productHeaderValue) 68 | { 69 | var client = new HttpClient(); 70 | client.DefaultRequestHeaders.Authorization = authenticationHeaderValue; 71 | client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(productHeaderValue)); 72 | 73 | return client; 74 | } 75 | 76 | protected Task SendValidRestApiRequest() => _client.GetAsync($"{GitHubConstants.GitHubRestApiUrl}/repos/brminnick/GitHubApiStatus"); 77 | 78 | protected Task SendValidSearchApiRequest() => _client.GetAsync($"{GitHubConstants.GitHubRestApiUrl}/search/code"); 79 | 80 | protected Task SendValidCodeScanningApiRequest() => _client.GetAsync($"{GitHubConstants.GitHubRestApiUrl}/repos/brminnick/GitHubApiStatus/code-scanning/alerts"); 81 | 82 | protected Task SendValidGraphQLApiRequest() 83 | { 84 | var graphQLRequest = new GraphQLRequest("query { user(login: \"brminnick\"){ name, company, createdAt}}"); 85 | var serializedGraphQLRequest = JsonConvert.SerializeObject(graphQLRequest); 86 | 87 | return _client.PostAsync(GitHubConstants.GitHubGraphQLApiUrl, new StringContent(serializedGraphQLRequest)); 88 | } 89 | 90 | static long GetTimeInUnixEpochSeconds(in DateTimeOffset dateTimeOffset) => dateTimeOffset.ToUnixTimeSeconds(); 91 | } -------------------------------------------------------------------------------- /src/GitHubApiStatus.UnitTests/Tests/GetApiRateLimitsTests_NoCancellationToken.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using GitStatus.Common; 3 | using NUnit.Framework; 4 | 5 | namespace GitHubApiStatus.UnitTests; 6 | 7 | class GetApiRateLimitsTests_NoCancellationToken : BaseTest 8 | { 9 | [Test] 10 | public async Task GetApiRateLimits_ValidRestApiRequest() 11 | { 12 | //Arrange 13 | RateLimitStatus restApiStatus_Initial, restApiStatus_Final; 14 | GitHubApiRateLimits gitHubApiRateLimits_Initial, gitHubApiRateLimits_Final; 15 | 16 | var startTime = DateTimeOffset.UtcNow; 17 | var gitHubApiStatusService = (GitHubApiStatusService)GitHubApiStatusService; 18 | 19 | //Act 20 | gitHubApiRateLimits_Initial = await gitHubApiStatusService.GetApiRateLimits().ConfigureAwait(false); 21 | restApiStatus_Initial = gitHubApiRateLimits_Initial.RestApi; 22 | 23 | await SendValidRestApiRequest().ConfigureAwait(false); 24 | 25 | gitHubApiRateLimits_Final = await gitHubApiStatusService.GetApiRateLimits().ConfigureAwait(false); 26 | restApiStatus_Final = gitHubApiRateLimits_Final.RestApi; 27 | 28 | //Assert 29 | Assert.Multiple(() => 30 | { 31 | Assert.That(restApiStatus_Initial, Is.Not.Null); 32 | Assert.That(restApiStatus_Initial.RateLimit, Is.EqualTo(5000)); 33 | Assert.That(restApiStatus_Initial.RemainingRequestCount, Is.GreaterThanOrEqualTo(0)); 34 | Assert.That(restApiStatus_Initial.RemainingRequestCount, Is.LessThanOrEqualTo(restApiStatus_Initial.RateLimit)); 35 | Assert.That(restApiStatus_Initial.RateLimitReset_DateTime.ToUnixTimeSeconds(), Is.EqualTo(restApiStatus_Initial.RateLimitReset_UnixEpochSeconds)); 36 | Assert.That(restApiStatus_Initial.RateLimitReset_DateTime, Is.GreaterThanOrEqualTo(startTime)); 37 | Assert.That(restApiStatus_Initial.RateLimitReset_UnixEpochSeconds, Is.GreaterThanOrEqualTo(startTime.ToUnixTimeSeconds())); 38 | 39 | Assert.That(restApiStatus_Final, Is.Not.Null); 40 | Assert.That(restApiStatus_Final.RateLimit, Is.EqualTo(5000)); 41 | Assert.That(restApiStatus_Final.RemainingRequestCount, Is.GreaterThanOrEqualTo(0)); 42 | Assert.That(restApiStatus_Final.RemainingRequestCount, Is.LessThanOrEqualTo(restApiStatus_Final.RateLimit)); 43 | Assert.That(restApiStatus_Final.RateLimitReset_DateTime.ToUnixTimeSeconds(), Is.EqualTo(restApiStatus_Final.RateLimitReset_UnixEpochSeconds)); 44 | Assert.That(restApiStatus_Final.RateLimitReset_DateTime, Is.GreaterThanOrEqualTo(startTime)); 45 | Assert.That(restApiStatus_Final.RateLimitReset_UnixEpochSeconds, Is.GreaterThanOrEqualTo(startTime.ToUnixTimeSeconds())); 46 | 47 | Assert.That(restApiStatus_Initial.RateLimit, Is.EqualTo(restApiStatus_Final.RateLimit)); 48 | Assert.That(restApiStatus_Initial.RateLimitReset_DateTime, Is.EqualTo(restApiStatus_Final.RateLimitReset_DateTime).Or.EqualTo(restApiStatus_Final.RateLimitReset_DateTime.Subtract(TimeSpan.FromSeconds(1)))); 49 | Assert.That(restApiStatus_Initial.RateLimitReset_TimeRemaining.TotalMilliseconds, Is.GreaterThanOrEqualTo(restApiStatus_Final.RateLimitReset_TimeRemaining.TotalMilliseconds)); 50 | Assert.That(restApiStatus_Initial.RateLimitReset_UnixEpochSeconds, Is.EqualTo(restApiStatus_Final.RateLimitReset_UnixEpochSeconds)); 51 | Assert.That(restApiStatus_Initial.RemainingRequestCount, Is.GreaterThan(restApiStatus_Final.RemainingRequestCount)); 52 | }); 53 | } 54 | 55 | [Test] 56 | public async Task GetApiRateLimits_ValidGraphQLApiRequest() 57 | { 58 | //Arrange 59 | RateLimitStatus graphQLApiStatus_Initial, graphQLApiStatus_Final; 60 | GitHubApiRateLimits gitHubApiRateLimits_Initial, gitHubApiRateLimits_Final; 61 | 62 | var startTime = DateTimeOffset.UtcNow; 63 | var gitHubApiStatusService = (GitHubApiStatusService)GitHubApiStatusService; 64 | 65 | //Act 66 | gitHubApiRateLimits_Initial = await gitHubApiStatusService.GetApiRateLimits().ConfigureAwait(false); 67 | graphQLApiStatus_Initial = gitHubApiRateLimits_Initial.GraphQLApi; 68 | 69 | await SendValidGraphQLApiRequest().ConfigureAwait(false); 70 | 71 | gitHubApiRateLimits_Final = await gitHubApiStatusService.GetApiRateLimits().ConfigureAwait(false); 72 | graphQLApiStatus_Final = gitHubApiRateLimits_Final.GraphQLApi; 73 | 74 | //Assert 75 | Assert.Multiple(() => 76 | { 77 | Assert.That(graphQLApiStatus_Initial, Is.Not.Null); 78 | Assert.That(graphQLApiStatus_Initial.RateLimit, Is.EqualTo(5000)); 79 | Assert.That(graphQLApiStatus_Initial.RemainingRequestCount, Is.GreaterThanOrEqualTo(0)); 80 | Assert.That(graphQLApiStatus_Initial.RemainingRequestCount, Is.LessThanOrEqualTo(graphQLApiStatus_Initial.RateLimit)); 81 | Assert.That(graphQLApiStatus_Initial.RateLimitReset_DateTime.ToUnixTimeSeconds(), Is.EqualTo(graphQLApiStatus_Initial.RateLimitReset_UnixEpochSeconds)); 82 | Assert.That(graphQLApiStatus_Initial.RateLimitReset_DateTime, Is.GreaterThanOrEqualTo(startTime)); 83 | Assert.That(graphQLApiStatus_Initial.RateLimitReset_UnixEpochSeconds, Is.GreaterThanOrEqualTo(startTime.ToUnixTimeSeconds())); 84 | 85 | Assert.That(graphQLApiStatus_Final, Is.Not.Null); 86 | Assert.That(graphQLApiStatus_Final.RateLimit, Is.EqualTo(5000)); 87 | Assert.That(graphQLApiStatus_Final.RemainingRequestCount, Is.GreaterThanOrEqualTo(0)); 88 | Assert.That(graphQLApiStatus_Final.RemainingRequestCount, Is.LessThanOrEqualTo(graphQLApiStatus_Final.RateLimit)); 89 | Assert.That(graphQLApiStatus_Final.RateLimitReset_DateTime.ToUnixTimeSeconds(), Is.EqualTo(graphQLApiStatus_Final.RateLimitReset_UnixEpochSeconds)); 90 | Assert.That(graphQLApiStatus_Final.RateLimitReset_DateTime, Is.GreaterThanOrEqualTo(startTime)); 91 | Assert.That(graphQLApiStatus_Final.RateLimitReset_UnixEpochSeconds, Is.GreaterThanOrEqualTo(startTime.ToUnixTimeSeconds())); 92 | 93 | Assert.That(graphQLApiStatus_Initial.RateLimit, Is.EqualTo(graphQLApiStatus_Final.RateLimit)); 94 | Assert.That(graphQLApiStatus_Initial.RateLimitReset_DateTime, Is.EqualTo(graphQLApiStatus_Final.RateLimitReset_DateTime).Or.EqualTo(graphQLApiStatus_Final.RateLimitReset_DateTime.Subtract(TimeSpan.FromSeconds(1)))); 95 | Assert.That(graphQLApiStatus_Initial.RateLimitReset_TimeRemaining.TotalMilliseconds, Is.GreaterThanOrEqualTo(graphQLApiStatus_Final.RateLimitReset_TimeRemaining.TotalMilliseconds)); 96 | Assert.That(graphQLApiStatus_Initial.RateLimitReset_UnixEpochSeconds, Is.EqualTo(graphQLApiStatus_Final.RateLimitReset_UnixEpochSeconds)); 97 | Assert.That(graphQLApiStatus_Initial.RemainingRequestCount, Is.GreaterThan(graphQLApiStatus_Final.RemainingRequestCount)); 98 | }); 99 | } 100 | 101 | [Test] 102 | public async Task GetApiRateLimits_ValidSearchApiRequest() 103 | { 104 | //Arrange 105 | RateLimitStatus searchApiStatus_Initial, searchApiStatus_Final; 106 | GitHubApiRateLimits gitHubApiRateLimits_Initial, gitHubApiRateLimits_Final; 107 | 108 | var startTime = DateTimeOffset.UtcNow; 109 | var gitHubApiStatusService = (GitHubApiStatusService)GitHubApiStatusService; 110 | 111 | //Act 112 | gitHubApiRateLimits_Initial = await gitHubApiStatusService.GetApiRateLimits().ConfigureAwait(false); 113 | searchApiStatus_Initial = gitHubApiRateLimits_Initial.SearchApi; 114 | 115 | await SendValidSearchApiRequest().ConfigureAwait(false); 116 | 117 | gitHubApiRateLimits_Final = await gitHubApiStatusService.GetApiRateLimits().ConfigureAwait(false); 118 | searchApiStatus_Final = gitHubApiRateLimits_Final.SearchApi; 119 | 120 | //Assert 121 | Assert.Multiple(() => 122 | { 123 | Assert.That(searchApiStatus_Initial, Is.Not.Null); 124 | Assert.That(searchApiStatus_Initial.RateLimit, Is.EqualTo(30)); 125 | Assert.That(searchApiStatus_Initial.RemainingRequestCount, Is.GreaterThanOrEqualTo(0)); 126 | Assert.That(searchApiStatus_Initial.RemainingRequestCount, Is.LessThanOrEqualTo(searchApiStatus_Initial.RateLimit)); 127 | Assert.That(searchApiStatus_Initial.RateLimitReset_DateTime.ToUnixTimeSeconds(), Is.EqualTo(searchApiStatus_Initial.RateLimitReset_UnixEpochSeconds)); 128 | Assert.That(searchApiStatus_Initial.RateLimitReset_DateTime, Is.GreaterThanOrEqualTo(startTime)); 129 | Assert.That(searchApiStatus_Initial.RateLimitReset_UnixEpochSeconds, Is.GreaterThanOrEqualTo(startTime.ToUnixTimeSeconds())); 130 | 131 | Assert.That(searchApiStatus_Final, Is.Not.Null); 132 | Assert.That(searchApiStatus_Final.RateLimit, Is.EqualTo(30)); 133 | Assert.That(searchApiStatus_Final.RemainingRequestCount, Is.GreaterThanOrEqualTo(0)); 134 | Assert.That(searchApiStatus_Final.RemainingRequestCount, Is.LessThanOrEqualTo(searchApiStatus_Final.RateLimit)); 135 | Assert.That(searchApiStatus_Final.RateLimitReset_DateTime.ToUnixTimeSeconds(), Is.EqualTo(searchApiStatus_Final.RateLimitReset_UnixEpochSeconds)); 136 | Assert.That(searchApiStatus_Final.RateLimitReset_DateTime, Is.GreaterThanOrEqualTo(startTime)); 137 | Assert.That(searchApiStatus_Final.RateLimitReset_UnixEpochSeconds, Is.GreaterThanOrEqualTo(startTime.ToUnixTimeSeconds())); 138 | 139 | if (searchApiStatus_Final.RateLimitReset_DateTime == searchApiStatus_Initial.RateLimitReset_DateTime) 140 | { 141 | Assert.That(searchApiStatus_Initial.RateLimit, Is.EqualTo(searchApiStatus_Final.RateLimit)); 142 | Assert.That(searchApiStatus_Initial.RateLimitReset_DateTime, Is.EqualTo(searchApiStatus_Final.RateLimitReset_DateTime).Or.EqualTo(searchApiStatus_Final.RateLimitReset_DateTime.Subtract(TimeSpan.FromSeconds(1)))); 143 | Assert.That(searchApiStatus_Initial.RateLimitReset_TimeRemaining.TotalMilliseconds, Is.GreaterThanOrEqualTo(searchApiStatus_Final.RateLimitReset_TimeRemaining.TotalMilliseconds)); 144 | Assert.That(searchApiStatus_Initial.RateLimitReset_UnixEpochSeconds, Is.EqualTo(searchApiStatus_Final.RateLimitReset_UnixEpochSeconds)); 145 | Assert.That(searchApiStatus_Initial.RemainingRequestCount, Is.GreaterThanOrEqualTo(searchApiStatus_Final.RemainingRequestCount)); 146 | } 147 | }); 148 | } 149 | 150 | [Test] 151 | public void GetApiRateLimits_InvalidBearerToken() 152 | { 153 | //Arrange 154 | var gitHubApiStatusService = (GitHubApiStatusService)GitHubApiStatusService; 155 | gitHubApiStatusService.SetAuthenticationHeaderValue(new AuthenticationHeaderValue(GitHubConstants.AuthScheme, "abc 123")); 156 | 157 | //Act 158 | 159 | //Assert 160 | var httpRequestException = Assert.ThrowsAsync(() => gitHubApiStatusService.GetApiRateLimits()); 161 | Assert.That(httpRequestException?.Message, Does.Contain("Unauthorized")); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/GitHubApiStatus.UnitTests/Tests/GetApiRateLimitsTests_WithCancellationToken.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Net.Http.Headers; 3 | using GitStatus.Common; 4 | using NUnit.Framework; 5 | 6 | namespace GitHubApiStatus.UnitTests; 7 | 8 | class GetApiRateLimitsTests_WithCancellationToken : BaseTest 9 | { 10 | [Test] 11 | public async Task GetApiRateLimits_ValidRestApiRequest() 12 | { 13 | //Arrange 14 | RateLimitStatus restApiStatus_Initial, restApiStatus_Final; 15 | GitHubApiRateLimits gitHubApiRateLimits_Initial, gitHubApiRateLimits_Final; 16 | 17 | var startTime = DateTimeOffset.UtcNow; 18 | var cancellationTokenSource = new CancellationTokenSource(); 19 | 20 | //Act 21 | gitHubApiRateLimits_Initial = await GitHubApiStatusService.GetApiRateLimits(cancellationTokenSource.Token).ConfigureAwait(false); 22 | restApiStatus_Initial = gitHubApiRateLimits_Initial.RestApi; 23 | 24 | await SendValidRestApiRequest().ConfigureAwait(false); 25 | 26 | gitHubApiRateLimits_Final = await GitHubApiStatusService.GetApiRateLimits(cancellationTokenSource.Token).ConfigureAwait(false); 27 | restApiStatus_Final = gitHubApiRateLimits_Final.RestApi; 28 | 29 | //Assert 30 | Assert.Multiple(() => 31 | { 32 | Assert.That(restApiStatus_Initial, Is.Not.Null); 33 | Assert.That(restApiStatus_Initial.RateLimit, Is.EqualTo(5000)); 34 | Assert.That(restApiStatus_Initial.RemainingRequestCount, Is.GreaterThanOrEqualTo(0)); 35 | Assert.That(restApiStatus_Initial.RemainingRequestCount, Is.LessThanOrEqualTo(restApiStatus_Initial.RateLimit)); 36 | Assert.That(restApiStatus_Initial.RateLimitReset_DateTime.ToUnixTimeSeconds(), Is.EqualTo(restApiStatus_Initial.RateLimitReset_UnixEpochSeconds)); 37 | Assert.That(restApiStatus_Initial.RateLimitReset_DateTime, Is.GreaterThanOrEqualTo(startTime)); 38 | Assert.That(restApiStatus_Initial.RateLimitReset_UnixEpochSeconds, Is.GreaterThanOrEqualTo(startTime.ToUnixTimeSeconds())); 39 | 40 | Assert.That(restApiStatus_Final, Is.Not.Null); 41 | Assert.That(restApiStatus_Final.RateLimit, Is.EqualTo(5000)); 42 | Assert.That(restApiStatus_Final.RemainingRequestCount, Is.GreaterThanOrEqualTo(0)); 43 | Assert.That(restApiStatus_Final.RemainingRequestCount, Is.LessThanOrEqualTo(restApiStatus_Final.RateLimit)); 44 | Assert.That(restApiStatus_Final.RateLimitReset_DateTime.ToUnixTimeSeconds(), Is.EqualTo(restApiStatus_Final.RateLimitReset_UnixEpochSeconds)); 45 | Assert.That(restApiStatus_Final.RateLimitReset_DateTime, Is.GreaterThanOrEqualTo(startTime)); 46 | Assert.That(restApiStatus_Final.RateLimitReset_UnixEpochSeconds, Is.GreaterThanOrEqualTo(startTime.ToUnixTimeSeconds())); 47 | 48 | Assert.That(restApiStatus_Initial.RateLimit, Is.EqualTo(restApiStatus_Final.RateLimit)); 49 | Assert.That(restApiStatus_Initial.RateLimitReset_DateTime, Is.EqualTo(restApiStatus_Final.RateLimitReset_DateTime)); 50 | Assert.That(restApiStatus_Initial.RateLimitReset_TimeRemaining, Is.GreaterThanOrEqualTo(restApiStatus_Final.RateLimitReset_TimeRemaining)); 51 | Assert.That(restApiStatus_Initial.RateLimitReset_UnixEpochSeconds, Is.EqualTo(restApiStatus_Final.RateLimitReset_UnixEpochSeconds)); 52 | Assert.That(restApiStatus_Initial.RemainingRequestCount, Is.GreaterThan(restApiStatus_Final.RemainingRequestCount)); 53 | }); 54 | } 55 | 56 | [Test] 57 | public async Task GetApiRateLimits_ValidGraphQLApiRequest() 58 | { 59 | //Arrange 60 | RateLimitStatus graphQLApiStatus_Initial, graphQLApiStatus_Final; 61 | GitHubApiRateLimits gitHubApiRateLimits_Initial, gitHubApiRateLimits_Final; 62 | 63 | var startTime = DateTimeOffset.UtcNow; 64 | var cancellationTokenSource = new CancellationTokenSource(); 65 | 66 | //Act 67 | gitHubApiRateLimits_Initial = await GitHubApiStatusService.GetApiRateLimits(cancellationTokenSource.Token).ConfigureAwait(false); 68 | graphQLApiStatus_Initial = gitHubApiRateLimits_Initial.GraphQLApi; 69 | 70 | await SendValidGraphQLApiRequest().ConfigureAwait(false); 71 | 72 | gitHubApiRateLimits_Final = await GitHubApiStatusService.GetApiRateLimits(cancellationTokenSource.Token).ConfigureAwait(false); 73 | graphQLApiStatus_Final = gitHubApiRateLimits_Final.GraphQLApi; 74 | 75 | //Assert 76 | Assert.Multiple(() => 77 | { 78 | Assert.That(graphQLApiStatus_Initial, Is.Not.Null); 79 | Assert.That(graphQLApiStatus_Initial.RateLimit, Is.EqualTo(5000)); 80 | Assert.That(graphQLApiStatus_Initial.RemainingRequestCount, Is.GreaterThanOrEqualTo(0)); 81 | Assert.That(graphQLApiStatus_Initial.RemainingRequestCount, Is.LessThanOrEqualTo(graphQLApiStatus_Initial.RateLimit)); 82 | Assert.That(graphQLApiStatus_Initial.RateLimitReset_DateTime.ToUnixTimeSeconds(), Is.EqualTo(graphQLApiStatus_Initial.RateLimitReset_UnixEpochSeconds)); 83 | Assert.That(graphQLApiStatus_Initial.RateLimitReset_DateTime, Is.GreaterThanOrEqualTo(startTime)); 84 | Assert.That(graphQLApiStatus_Initial.RateLimitReset_UnixEpochSeconds, Is.GreaterThanOrEqualTo(startTime.ToUnixTimeSeconds())); 85 | 86 | Assert.That(graphQLApiStatus_Final, Is.Not.Null); 87 | Assert.That(graphQLApiStatus_Final.RateLimit, Is.EqualTo(5000)); 88 | Assert.That(graphQLApiStatus_Final.RemainingRequestCount, Is.GreaterThanOrEqualTo(0)); 89 | Assert.That(graphQLApiStatus_Final.RemainingRequestCount, Is.LessThanOrEqualTo(graphQLApiStatus_Final.RateLimit)); 90 | Assert.That(graphQLApiStatus_Final.RateLimitReset_DateTime.ToUnixTimeSeconds(), Is.EqualTo(graphQLApiStatus_Final.RateLimitReset_UnixEpochSeconds)); 91 | Assert.That(graphQLApiStatus_Final.RateLimitReset_DateTime, Is.GreaterThanOrEqualTo(startTime)); 92 | Assert.That(graphQLApiStatus_Final.RateLimitReset_UnixEpochSeconds, Is.GreaterThanOrEqualTo(startTime.ToUnixTimeSeconds())); 93 | 94 | Assert.That(graphQLApiStatus_Initial.RateLimit, Is.EqualTo(graphQLApiStatus_Final.RateLimit)); 95 | Assert.That(graphQLApiStatus_Initial.RateLimitReset_DateTime, Is.EqualTo(graphQLApiStatus_Final.RateLimitReset_DateTime)); 96 | Assert.That(graphQLApiStatus_Initial.RateLimitReset_TimeRemaining, Is.GreaterThanOrEqualTo(graphQLApiStatus_Final.RateLimitReset_TimeRemaining)); 97 | Assert.That(graphQLApiStatus_Initial.RateLimitReset_UnixEpochSeconds, Is.EqualTo(graphQLApiStatus_Final.RateLimitReset_UnixEpochSeconds)); 98 | Assert.That(graphQLApiStatus_Initial.RemainingRequestCount, Is.GreaterThan(graphQLApiStatus_Final.RemainingRequestCount)); 99 | }); 100 | } 101 | 102 | [Test] 103 | public async Task GetApiRateLimits_ValidSearchApiRequest() 104 | { 105 | //Arrange 106 | RateLimitStatus searchApiStatus_Initial, searchApiStatus_Final; 107 | GitHubApiRateLimits gitHubApiRateLimits_Initial, gitHubApiRateLimits_Final; 108 | 109 | var startTime = DateTimeOffset.UtcNow; 110 | var cancellationTokenSource = new CancellationTokenSource(); 111 | 112 | //Act 113 | gitHubApiRateLimits_Initial = await GitHubApiStatusService.GetApiRateLimits(cancellationTokenSource.Token).ConfigureAwait(false); 114 | searchApiStatus_Initial = gitHubApiRateLimits_Initial.SearchApi; 115 | 116 | await SendValidSearchApiRequest().ConfigureAwait(false); 117 | 118 | gitHubApiRateLimits_Final = await GitHubApiStatusService.GetApiRateLimits(cancellationTokenSource.Token).ConfigureAwait(false); 119 | searchApiStatus_Final = gitHubApiRateLimits_Final.SearchApi; 120 | 121 | //Assert 122 | Assert.Multiple(() => 123 | { 124 | Assert.That(searchApiStatus_Initial, Is.Not.Null); 125 | Assert.That(searchApiStatus_Initial.RateLimit, Is.EqualTo(30)); 126 | Assert.That(searchApiStatus_Initial.RemainingRequestCount, Is.GreaterThanOrEqualTo(0)); 127 | Assert.That(searchApiStatus_Initial.RemainingRequestCount, Is.LessThanOrEqualTo(searchApiStatus_Initial.RateLimit)); 128 | Assert.That(searchApiStatus_Initial.RateLimitReset_DateTime.ToUnixTimeSeconds(), Is.EqualTo(searchApiStatus_Initial.RateLimitReset_UnixEpochSeconds)); 129 | Assert.That(searchApiStatus_Initial.RateLimitReset_DateTime, Is.GreaterThanOrEqualTo(startTime)); 130 | Assert.That(searchApiStatus_Initial.RateLimitReset_UnixEpochSeconds, Is.GreaterThanOrEqualTo(startTime.ToUnixTimeSeconds())); 131 | 132 | Assert.That(searchApiStatus_Final, Is.Not.Null); 133 | Assert.That(searchApiStatus_Final.RateLimit, Is.EqualTo(30)); 134 | Assert.That(searchApiStatus_Final.RemainingRequestCount, Is.GreaterThanOrEqualTo(0)); 135 | Assert.That(searchApiStatus_Final.RemainingRequestCount, Is.LessThanOrEqualTo(searchApiStatus_Final.RateLimit)); 136 | Assert.That(searchApiStatus_Final.RateLimitReset_DateTime.ToUnixTimeSeconds(), Is.EqualTo(searchApiStatus_Final.RateLimitReset_UnixEpochSeconds)); 137 | Assert.That(searchApiStatus_Final.RateLimitReset_DateTime, Is.GreaterThanOrEqualTo(startTime)); 138 | Assert.That(searchApiStatus_Final.RateLimitReset_UnixEpochSeconds, Is.GreaterThanOrEqualTo(startTime.ToUnixTimeSeconds())); 139 | 140 | if (searchApiStatus_Final.RateLimitReset_DateTime == searchApiStatus_Initial.RateLimitReset_DateTime) 141 | { 142 | Assert.That(searchApiStatus_Initial.RateLimit, Is.EqualTo(searchApiStatus_Final.RateLimit)); 143 | Assert.That(searchApiStatus_Initial.RateLimitReset_DateTime, Is.EqualTo(searchApiStatus_Final.RateLimitReset_DateTime)); 144 | Assert.That(searchApiStatus_Initial.RateLimitReset_TimeRemaining, Is.GreaterThanOrEqualTo(searchApiStatus_Final.RateLimitReset_TimeRemaining)); 145 | Assert.That(searchApiStatus_Initial.RateLimitReset_UnixEpochSeconds, Is.EqualTo(searchApiStatus_Final.RateLimitReset_UnixEpochSeconds)); 146 | Assert.That(searchApiStatus_Initial.RemainingRequestCount, Is.GreaterThanOrEqualTo(searchApiStatus_Final.RemainingRequestCount)); 147 | } 148 | }); 149 | } 150 | 151 | [Test] 152 | public void GetApiRateLimits_CancelledRequest() 153 | { 154 | //Arrange 155 | var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(1)); 156 | var authenticationHeaderValue = new AuthenticationHeaderValue(GitHubConstants.AuthScheme, GitHubConstants.PersonalAccessToken); 157 | 158 | //Act 159 | 160 | //Assert 161 | Assert.That(() => GitHubApiStatusService.GetApiRateLimits(cancellationTokenSource.Token), Throws.TypeOf()); 162 | } 163 | 164 | [Test] 165 | public void GetApiRateLimits_InvalidBearerToken() 166 | { 167 | //Arrange 168 | var cancellationTokenSource = new CancellationTokenSource(); 169 | GitHubApiStatusService.SetAuthenticationHeaderValue(new AuthenticationHeaderValue(GitHubConstants.AuthScheme, "abc 123")); 170 | 171 | //Act 172 | 173 | //Assert 174 | var httpRequestException = Assert.ThrowsAsync(() => GitHubApiStatusService.GetApiRateLimits(cancellationTokenSource.Token)); 175 | Assert.That(httpRequestException?.Message, Does.Contain("Unauthorized")); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/GitHubApiStatus.UnitTests/Tests/GetRateLimitRestDateTimeTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Net.Http.Headers; 3 | using NUnit.Framework; 4 | 5 | namespace GitHubApiStatus.UnitTests; 6 | 7 | class GetRateLimitRestDateTimeTests : BaseTest 8 | { 9 | [Test] 10 | public void GetRateLimitResetDateTime_ValidHttpResponseHeaders() 11 | { 12 | //Act 13 | DateTimeOffset rateLimitResetDateTime_Actual; 14 | 15 | const int rateLimit = 5000; 16 | var rateLimitResetDateTime_Expected = DateTime.UtcNow.Add(TimeSpan.FromMinutes(20)); 17 | 18 | var validHttpResponseHeaders = CreateHttpResponseHeaders(rateLimit, rateLimitResetDateTime_Expected, rateLimit - 5); 19 | 20 | //Act 21 | rateLimitResetDateTime_Actual = GitHubApiStatusService.GetRateLimitResetDateTime(validHttpResponseHeaders); 22 | 23 | //Assert 24 | Assert.Multiple(() => 25 | { 26 | Assert.That(rateLimitResetDateTime_Expected.Second, Is.EqualTo(rateLimitResetDateTime_Actual.Second)); 27 | Assert.That(rateLimitResetDateTime_Expected.Minute, Is.EqualTo(rateLimitResetDateTime_Actual.Minute)); 28 | Assert.That(rateLimitResetDateTime_Expected.Hour, Is.EqualTo(rateLimitResetDateTime_Actual.Hour)); 29 | Assert.That(rateLimitResetDateTime_Expected.DayOfYear, Is.EqualTo(rateLimitResetDateTime_Actual.DayOfYear)); 30 | Assert.That(rateLimitResetDateTime_Expected.Year, Is.EqualTo(rateLimitResetDateTime_Actual.Year)); 31 | }); 32 | } 33 | 34 | [Test] 35 | public void GetRateLimitResetDateTime_InvalidHttpResponseHeaders() 36 | { 37 | //Arrange 38 | var invalidHttpResponseMessage = new HttpResponseMessage(); 39 | 40 | //Act 41 | 42 | //Assert 43 | Assert.That(() => GitHubApiStatusService.GetRateLimitResetDateTime(invalidHttpResponseMessage.Headers), Throws.TypeOf()); 44 | } 45 | 46 | [Test] 47 | public void GetRateLimitResetDateTime_NullHttpResponseHeaders() 48 | { 49 | //Arrange 50 | HttpResponseHeaders? nullHttpResponseHeaders = null; 51 | 52 | //Act 53 | 54 | //Assert 55 | #pragma warning disable CS8604 // Possible null reference argument. 56 | Assert.That(() => GitHubApiStatusService.GetRateLimitResetDateTime(nullHttpResponseHeaders), Throws.TypeOf()); 57 | #pragma warning restore CS8604 // Possible null reference argument. 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /src/GitHubApiStatus.UnitTests/Tests/GetRateLimitTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Net.Http.Headers; 3 | using NUnit.Framework; 4 | 5 | namespace GitHubApiStatus.UnitTests; 6 | 7 | class GetRateLimitTests : BaseTest 8 | { 9 | [Test] 10 | public void GetRateLimit_ValidHttpResponseHeaders() 11 | { 12 | //Act 13 | const int rateLimit_Expected = 5000; 14 | int rateLimit_Actual; 15 | 16 | var validHttpResponseHeaders = CreateHttpResponseHeaders(rateLimit_Expected, DateTimeOffset.UtcNow, rateLimit_Expected - 5); 17 | 18 | //Act 19 | rateLimit_Actual = GitHubApiStatusService.GetRateLimit(validHttpResponseHeaders); 20 | 21 | //Assert 22 | Assert.That(rateLimit_Actual, Is.EqualTo(rateLimit_Expected)); 23 | } 24 | 25 | [Test] 26 | public void GetRateLimit_InvalidHttpResponseHeaders() 27 | { 28 | //Arrange 29 | var invalidHttpResponseMessage = new HttpResponseMessage(); 30 | 31 | //Act 32 | 33 | //Assert 34 | Assert.That(() => GitHubApiStatusService.GetRateLimit(invalidHttpResponseMessage.Headers), Throws.TypeOf()); 35 | } 36 | 37 | [Test] 38 | public void GetRateLimit_NullHttpResponseHeaders() 39 | { 40 | //Arrange 41 | HttpResponseHeaders? nullHttpResponseHeaders = null; 42 | 43 | //Act 44 | 45 | //Assert 46 | #pragma warning disable CS8604 // Possible null reference argument. 47 | Assert.That(() => GitHubApiStatusService.GetRateLimit(nullHttpResponseHeaders), Throws.TypeOf()); 48 | #pragma warning restore CS8604 // Possible null reference argument. 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /src/GitHubApiStatus.UnitTests/Tests/GetRateLimitTimeRemainingTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Net.Http.Headers; 3 | using NUnit.Framework; 4 | 5 | namespace GitHubApiStatus.UnitTests; 6 | 7 | class GetRateLimitTimeRemainingTests : BaseTest 8 | { 9 | [Test] 10 | public void GetRemainingRequestCount_ValidHttpResponseHeaders() 11 | { 12 | //Act 13 | const int rateLimit = 5000; 14 | TimeSpan rateLimitTimeRemaining_Actual; 15 | 16 | var rateLimitTimeRemaining_Expected = TimeSpan.FromMinutes(45); 17 | var rateLimitResetDateTime = DateTimeOffset.UtcNow.Add(rateLimitTimeRemaining_Expected); 18 | 19 | var validHttpResponseHeaders = CreateHttpResponseHeaders(rateLimit, rateLimitResetDateTime, rateLimit - 5); 20 | 21 | //Act 22 | rateLimitTimeRemaining_Actual = GitHubApiStatusService.GetRateLimitTimeRemaining(validHttpResponseHeaders); 23 | 24 | //Assert 25 | Assert.Multiple(() => 26 | { 27 | Assert.That(rateLimitTimeRemaining_Expected, Is.GreaterThan(rateLimitTimeRemaining_Actual)); 28 | Assert.That(rateLimitTimeRemaining_Expected.Subtract(TimeSpan.FromSeconds(2)), Is.LessThan(rateLimitTimeRemaining_Actual)); 29 | }); 30 | } 31 | 32 | [Test] 33 | public void GetRemainingRequestCount_InvalidHttpResponseHeaders() 34 | { 35 | //Arrange 36 | var invalidHttpResponseMessage = new HttpResponseMessage(); 37 | 38 | //Act 39 | 40 | //Assert 41 | Assert.That(() => GitHubApiStatusService.GetRateLimitTimeRemaining(invalidHttpResponseMessage.Headers), Throws.TypeOf()); 42 | } 43 | 44 | [Test] 45 | public void GetRemainingRequestCount_NullHttpResponseHeaders() 46 | { 47 | //Arrange 48 | HttpResponseHeaders? nullHttpResponseHeaders = null; 49 | 50 | //Act 51 | 52 | //Assert 53 | #pragma warning disable CS8604 // Possible null reference argument. 54 | Assert.That(() => GitHubApiStatusService.GetRateLimitTimeRemaining(nullHttpResponseHeaders), Throws.TypeOf()); 55 | #pragma warning restore CS8604 // Possible null reference argument. 56 | } 57 | } 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/GitHubApiStatus.UnitTests/Tests/GetRemainingRequestCountTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Net.Http.Headers; 3 | using NUnit.Framework; 4 | 5 | namespace GitHubApiStatus.UnitTests; 6 | 7 | class GetRemainingRequestCountTests : BaseTest 8 | { 9 | [Test] 10 | public void GetRemainingRequestCount_ValidHttpResponseHeaders() 11 | { 12 | //Act 13 | const int rateLimit = 5000; 14 | const int remainingRequestCount_Expected = 100; 15 | 16 | int remainingRequestCount_Actual; 17 | 18 | var validHttpResponseHeaders = CreateHttpResponseHeaders(rateLimit, DateTimeOffset.UtcNow, remainingRequestCount_Expected); 19 | 20 | //Act 21 | remainingRequestCount_Actual = GitHubApiStatusService.GetRemainingRequestCount(validHttpResponseHeaders); 22 | 23 | //Assert 24 | Assert.That(remainingRequestCount_Actual, Is.EqualTo(remainingRequestCount_Expected)); 25 | } 26 | 27 | [Test] 28 | public void GetRemainingRequestCount_InvalidHttpResponseHeaders() 29 | { 30 | //Arrange 31 | var invalidHttpResponseMessage = new HttpResponseMessage(); 32 | 33 | //Act 34 | 35 | //Assert 36 | Assert.That(() => GitHubApiStatusService.GetRemainingRequestCount(invalidHttpResponseMessage.Headers), Throws.TypeOf()); 37 | } 38 | 39 | [Test] 40 | public void GetRemainingRequestCount_NullHttpResponseHeaders() 41 | { 42 | //Arrange 43 | HttpResponseHeaders? nullHttpResponseHeaders = null; 44 | 45 | //Act 46 | 47 | //Assert 48 | #pragma warning disable CS8604 // Possible null reference argument. 49 | Assert.That(() => GitHubApiStatusService.GetRemainingRequestCount(nullHttpResponseHeaders), Throws.TypeOf()); 50 | #pragma warning restore CS8604 // Possible null reference argument. 51 | } 52 | } 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/GitHubApiStatus.UnitTests/Tests/GitHubApiStatusServiceConstructorTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using GitStatus.Common; 3 | using NUnit.Framework; 4 | 5 | namespace GitHubApiStatus.UnitTests; 6 | 7 | class GitHubApiStatusServiceConstructorTests : BaseTest 8 | { 9 | [Test] 10 | public void DefaultConstructorTest() 11 | { 12 | //Arrange 13 | var githubApiStatusService = new GitHubApiStatusService(); 14 | 15 | //Act 16 | 17 | //Assert 18 | Assert.That(() => githubApiStatusService.GetApiRateLimits(), Throws.TypeOf()); 19 | } 20 | 21 | [Test] 22 | public void NullAuthenticationHeaderValue() 23 | { 24 | //Arrange 25 | var productHeaderValue = new ProductHeaderValue(nameof(GitHubApiStatus)); 26 | 27 | //Act 28 | 29 | //Assert 30 | #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. 31 | Assert.That(() => new GitHubApiStatusService(null, productHeaderValue), Throws.TypeOf()); 32 | #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. 33 | } 34 | 35 | [Test] 36 | public void NullProductHeaderValue() 37 | { 38 | //Arrange 39 | var authenticationHeaderValue = new AuthenticationHeaderValue(GitHubConstants.AuthScheme, GitHubConstants.PersonalAccessToken); 40 | 41 | //Act 42 | 43 | //Assert 44 | #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. 45 | Assert.That(() => new GitHubApiStatusService(authenticationHeaderValue, null), Throws.TypeOf()); 46 | #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. 47 | } 48 | 49 | [Test] 50 | public void NullHttpClient() 51 | { 52 | //Arrange 53 | 54 | //Act 55 | 56 | //Assert 57 | #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. 58 | Assert.That(() => new GitHubApiStatusService(null), Throws.TypeOf()); 59 | #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. 60 | } 61 | 62 | [TestCase("Basic")] 63 | [TestCase("Oauth")] 64 | [TestCase("Digest")] 65 | public void InvalidSchemeAuthenticationHeaderValue(string scheme) 66 | { 67 | //Arrange 68 | var productHeaderValue = new ProductHeaderValue(nameof(GitHubApiStatus)); 69 | var authenticationHeaderValue = new AuthenticationHeaderValue(scheme, GitHubConstants.PersonalAccessToken); 70 | 71 | //Act 72 | 73 | //Assert 74 | Assert.That(() => new GitHubApiStatusService(authenticationHeaderValue, productHeaderValue), Throws.TypeOf()); 75 | } 76 | 77 | [Test] 78 | public async Task BEARERSchemeAuthenticationHeaderValue() 79 | { 80 | //Arrange 81 | var productHeaderValue = new ProductHeaderValue(nameof(GitHubApiStatus)); 82 | var authenticationHeaderValue = new AuthenticationHeaderValue(GitHubConstants.AuthScheme.ToUpper(), GitHubConstants.PersonalAccessToken); 83 | var gitHubApiStatusService = new GitHubApiStatusService(authenticationHeaderValue, productHeaderValue); 84 | 85 | //Act 86 | var apiRateLimits = await gitHubApiStatusService.GetApiRateLimits().ConfigureAwait(false); 87 | 88 | //Assert 89 | Assert.Multiple(() => 90 | { 91 | Assert.That(gitHubApiStatusService, Is.Not.Null); 92 | Assert.That(apiRateLimits, Is.Not.Null); 93 | Assert.That(apiRateLimits.AppManifestConfiguration, Is.Not.Null); 94 | Assert.That(apiRateLimits.CodeScanningUpload, Is.Not.Null); 95 | Assert.That(apiRateLimits.GraphQLApi, Is.Not.Null); 96 | Assert.That(apiRateLimits.RestApi, Is.Not.Null); 97 | Assert.That(apiRateLimits.SearchApi, Is.Not.Null); 98 | Assert.That(apiRateLimits.SourceImport, Is.Not.Null); 99 | }); 100 | } 101 | 102 | [Test] 103 | public async Task BEARERSchemeHttpClient() 104 | { 105 | //Arrange 106 | var httpClient = CreateGitHubHttpClient(new AuthenticationHeaderValue(GitHubConstants.AuthScheme.ToUpper(), GitHubConstants.PersonalAccessToken), new ProductHeaderValue(nameof(GitHubApiStatus))); 107 | var gitHubApiStatusService = new GitHubApiStatusService(httpClient); 108 | 109 | //Act 110 | var apiRateLimits = await gitHubApiStatusService.GetApiRateLimits().ConfigureAwait(false); 111 | 112 | //Assert 113 | Assert.Multiple(() => 114 | { 115 | Assert.That(gitHubApiStatusService, Is.Not.Null); 116 | Assert.That(apiRateLimits, Is.Not.Null); 117 | Assert.That(apiRateLimits.AppManifestConfiguration, Is.Not.Null); 118 | Assert.That(apiRateLimits.CodeScanningUpload, Is.Not.Null); 119 | Assert.That(apiRateLimits.GraphQLApi, Is.Not.Null); 120 | Assert.That(apiRateLimits.RestApi, Is.Not.Null); 121 | Assert.That(apiRateLimits.SearchApi, Is.Not.Null); 122 | Assert.That(apiRateLimits.SourceImport, Is.Not.Null); 123 | }); 124 | } 125 | 126 | [Test] 127 | public async Task ValidProductHeaderValue() 128 | { 129 | //Arrange 130 | var authenticationHeaderValue = new AuthenticationHeaderValue(GitHubConstants.AuthScheme, GitHubConstants.PersonalAccessToken); 131 | var productHeaderValue = new ProductHeaderValue(nameof(GitHubApiStatus)); 132 | 133 | var gitHubApiStatusService = new GitHubApiStatusService(authenticationHeaderValue, productHeaderValue); 134 | 135 | //Act 136 | var apiRateLimits = await gitHubApiStatusService.GetApiRateLimits().ConfigureAwait(false); 137 | 138 | //Assert 139 | Assert.Multiple(() => 140 | { 141 | Assert.That(gitHubApiStatusService, Is.Not.Null); 142 | Assert.That(apiRateLimits, Is.Not.Null); 143 | Assert.That(apiRateLimits.AppManifestConfiguration, Is.Not.Null); 144 | Assert.That(apiRateLimits.CodeScanningUpload, Is.Not.Null); 145 | Assert.That(apiRateLimits.GraphQLApi, Is.Not.Null); 146 | Assert.That(apiRateLimits.RestApi, Is.Not.Null); 147 | Assert.That(apiRateLimits.SearchApi, Is.Not.Null); 148 | Assert.That(apiRateLimits.SourceImport, Is.Not.Null); 149 | }); 150 | } 151 | } 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /src/GitHubApiStatus.UnitTests/Tests/HasReachedMaximumApiCallLimitTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Net.Http.Headers; 3 | using NUnit.Framework; 4 | 5 | namespace GitHubApiStatus.UnitTests; 6 | 7 | class HasReachedMaximumApiCallLimitTests : BaseTest 8 | { 9 | [Test] 10 | public void HasReachedMaximumApiCallLimitTests_ValidHttpResponseHeaders_True() 11 | { 12 | //Act 13 | const int rateLimit = 5000; 14 | const int rateLimitRemaining = 0; 15 | const bool hasReachedMaximumApiCallLimit_Expected = true; 16 | 17 | bool hasReachedMaximumApiCallLimit_Actual; 18 | 19 | var validHttpResponseHeaders = CreateHttpResponseHeaders(rateLimit, DateTimeOffset.UtcNow, rateLimitRemaining); 20 | 21 | //Act 22 | hasReachedMaximumApiCallLimit_Actual = GitHubApiStatusService.HasReachedMaximumApiCallLimit(validHttpResponseHeaders); 23 | 24 | //Assert 25 | Assert.That(hasReachedMaximumApiCallLimit_Actual, Is.EqualTo(hasReachedMaximumApiCallLimit_Expected)); 26 | } 27 | 28 | [Test] 29 | public void HasReachedMaximumApiCallLimitTests_ValidHttpResponseHeaders_False() 30 | { 31 | //Act 32 | const int rateLimit = 5000; 33 | const int rateLimitRemaining = 10; 34 | const bool hasReachedMaximumApiCallLimit_Expected = false; 35 | 36 | bool hasReachedMaximumApiCallLimit_Actual; 37 | 38 | var validHttpResponseHeaders = CreateHttpResponseHeaders(rateLimit, DateTimeOffset.UtcNow, rateLimitRemaining); 39 | 40 | //Act 41 | hasReachedMaximumApiCallLimit_Actual = GitHubApiStatusService.HasReachedMaximumApiCallLimit(validHttpResponseHeaders); 42 | 43 | //Assert 44 | Assert.That(hasReachedMaximumApiCallLimit_Actual, Is.EqualTo(hasReachedMaximumApiCallLimit_Expected)); 45 | } 46 | 47 | [Test] 48 | public void HasReachedMaximumApiCallLimitTests_InvalidHttpResponseHeaders() 49 | { 50 | //Arrange 51 | var invalidHttpResponseMessage = new HttpResponseMessage(); 52 | 53 | //Act 54 | 55 | //Assert 56 | Assert.That(() => GitHubApiStatusService.HasReachedMaximumApiCallLimit(invalidHttpResponseMessage.Headers), Throws.TypeOf()); 57 | } 58 | 59 | [Test] 60 | public void HasReachedMaximumApiCallLimitTests_NullHttpResponseHeaders() 61 | { 62 | //Arrange 63 | HttpResponseHeaders? nullHttpResponseHeaders = null; 64 | 65 | //Act 66 | 67 | //Assert 68 | #pragma warning disable CS8604 // Possible null reference argument. 69 | Assert.That(() => GitHubApiStatusService.HasReachedMaximumApiCallLimit(nullHttpResponseHeaders), Throws.TypeOf()); 70 | #pragma warning restore CS8604 // Possible null reference argument. 71 | } 72 | } 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/GitHubApiStatus.UnitTests/Tests/HttpResponseHeadersExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using NUnit.Framework; 3 | 4 | namespace GitHubApiStatus.UnitTests; 5 | 6 | class HttpResponseHeadersExtensionsTests : BaseTest 7 | { 8 | [Test] 9 | public void DoesContainGitHubRateLimitHeaderTest() 10 | { 11 | //Arrange 12 | bool doesContainGitHubRateLimitHeader_true, doesContainGitHubRateLimitHeader_false; 13 | 14 | var validHttpResponseHeaders = CreateHttpResponseHeaders(500, DateTimeOffset.UtcNow.AddHours(1), 450); 15 | var invalidHttpResponseHeaders = new HttpResponseMessage().Headers; 16 | 17 | //Act 18 | doesContainGitHubRateLimitHeader_true = validHttpResponseHeaders.DoesContainGitHubRateLimitHeader(); 19 | doesContainGitHubRateLimitHeader_false = invalidHttpResponseHeaders.DoesContainGitHubRateLimitHeader(); 20 | 21 | //Assert 22 | Assert.Multiple(() => 23 | { 24 | Assert.That(doesContainGitHubRateLimitHeader_true, Is.True); 25 | Assert.That(doesContainGitHubRateLimitHeader_false, Is.False); 26 | }); 27 | } 28 | 29 | [Test] 30 | public void DoesContainGitHubRateLimitResetHeaderTest() 31 | { 32 | //Arrange 33 | bool doesContainGitHubRateLimitResetHeader_true, doesContainGitHubRateLimitResetHeader_false; 34 | 35 | var validHttpResponseHeaders = CreateHttpResponseHeaders(500, DateTimeOffset.UtcNow.AddHours(1), 450); 36 | var invalidHttpResponseHeaders = new HttpResponseMessage().Headers; 37 | 38 | //Act 39 | doesContainGitHubRateLimitResetHeader_true = validHttpResponseHeaders.DoesContainGitHubRateLimitResetHeader(); 40 | doesContainGitHubRateLimitResetHeader_false = invalidHttpResponseHeaders.DoesContainGitHubRateLimitResetHeader(); 41 | 42 | //Assert 43 | Assert.Multiple(() => 44 | { 45 | Assert.That(doesContainGitHubRateLimitResetHeader_true, Is.True); 46 | Assert.That(doesContainGitHubRateLimitResetHeader_false, Is.False); 47 | }); 48 | } 49 | 50 | [Test] 51 | public void DoesContainGitHubRateLimitRemainingHeaderTest() 52 | { 53 | //Arrange 54 | bool doesContainGitHubRateLimitRemainingHeader_true, doesContainGitHubRateLimitRemainingHeader_false; 55 | 56 | var validHttpResponseHeaders = CreateHttpResponseHeaders(500, DateTimeOffset.UtcNow.AddHours(1), 450); 57 | var invalidHttpResponseHeaders = new HttpResponseMessage().Headers; 58 | 59 | //Act 60 | doesContainGitHubRateLimitRemainingHeader_true = validHttpResponseHeaders.DoesContainGitHubRateLimitRemainingHeader(); 61 | doesContainGitHubRateLimitRemainingHeader_false = invalidHttpResponseHeaders.DoesContainGitHubRateLimitRemainingHeader(); 62 | 63 | //Assert 64 | Assert.Multiple(() => 65 | { 66 | Assert.That(doesContainGitHubRateLimitRemainingHeader_true, Is.True); 67 | Assert.That(doesContainGitHubRateLimitRemainingHeader_false, Is.False); 68 | }); 69 | } 70 | } 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/GitHubApiStatus.UnitTests/Tests/IsAbuseRateLimitTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace GitHubApiStatus.UnitTests; 4 | 5 | class IsAbuseRateLimitTest : BaseTest 6 | { 7 | [Test] 8 | public void IsAbuseRateLimit() 9 | { 10 | //Arrange 11 | var httpResponseHeaders = CreateHttpResponseHeaders(500, DateTimeOffset.UtcNow, 0, isAbuseRateLimit: true); 12 | 13 | //Act 14 | var isAbuseRateLimit = GitHubApiStatusService.IsAbuseRateLimit(httpResponseHeaders, out var delta); 15 | 16 | //Assert 17 | Assert.Multiple(() => 18 | { 19 | Assert.That(isAbuseRateLimit, Is.True); 20 | Assert.That(delta, Is.Not.Null); 21 | }); 22 | } 23 | 24 | [Test] 25 | public void IsNotAbuseRateLimit() 26 | { 27 | //Arrange 28 | var httpResponseHeaders = CreateHttpResponseHeaders(500, DateTimeOffset.UtcNow, 0); 29 | 30 | //Act 31 | var isAbuseRateLimit = GitHubApiStatusService.IsAbuseRateLimit(httpResponseHeaders, out TimeSpan? delta); 32 | 33 | //Assert 34 | Assert.Multiple(() => 35 | { 36 | Assert.That(isAbuseRateLimit, Is.False); 37 | Assert.That(delta, Is.Null); 38 | }); 39 | } 40 | 41 | [Test] 42 | public void NullHttpResponseHeaders() 43 | { 44 | //Arrange 45 | 46 | //Act 47 | 48 | //Assert 49 | #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. 50 | Assert.That(() => GitHubApiStatusService.IsAbuseRateLimit(null, out _), Throws.TypeOf()); 51 | #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. 52 | } 53 | } 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/GitHubApiStatus.UnitTests/Tests/IsResponseFromAuthenticatedRequestTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Net.Http.Headers; 3 | using NUnit.Framework; 4 | 5 | namespace GitHubApiStatus.UnitTests; 6 | 7 | class IsAuthenticatedTests : BaseTest 8 | { 9 | [Test] 10 | public void IsResponseFromAuthenticatedRequest_ValidHttpResponseHeaders_True() 11 | { 12 | //Act 13 | bool isUserAuthenticated_Actual; 14 | 15 | const int rateLimit = 5000; 16 | const int rateLimitRemaining = 0; 17 | const bool isUserAuthenticated_Expected = true; 18 | 19 | var validHttpResponseHeaders = CreateHttpResponseHeaders(rateLimit, DateTimeOffset.UtcNow, rateLimitRemaining, isAuthenticated: isUserAuthenticated_Expected); 20 | 21 | //Act 22 | isUserAuthenticated_Actual = GitHubApiStatusService.IsResponseFromAuthenticatedRequest(validHttpResponseHeaders); 23 | 24 | //Assert 25 | Assert.That(isUserAuthenticated_Actual, Is.EqualTo(isUserAuthenticated_Expected)); 26 | } 27 | 28 | [Test] 29 | public void IsResponseFromAuthenticatedRequest_ValidHttpResponseHeaders_False() 30 | { 31 | //Act 32 | bool isUserAuthenticated_Actual; 33 | 34 | const int rateLimit = 5000; 35 | const int rateLimitRemaining = 10; 36 | const bool isUserAuthenticated_Expected = false; 37 | 38 | var validHttpResponseHeaders = CreateHttpResponseHeaders(rateLimit, DateTimeOffset.UtcNow, rateLimitRemaining, isAuthenticated: isUserAuthenticated_Expected); 39 | 40 | //Act 41 | isUserAuthenticated_Actual = GitHubApiStatusService.IsResponseFromAuthenticatedRequest(validHttpResponseHeaders); 42 | 43 | //Assert 44 | Assert.That(isUserAuthenticated_Actual, Is.EqualTo(isUserAuthenticated_Expected)); 45 | } 46 | 47 | [Test] 48 | public void IsResponseFromAuthenticatedRequest_InvalidHttpResponseHeaders() 49 | { 50 | //Arrange 51 | var invalidHttpResponseMessage = new HttpResponseMessage(); 52 | 53 | //Act 54 | var isUserAuthenticated = GitHubApiStatusService.IsResponseFromAuthenticatedRequest(invalidHttpResponseMessage.Headers); 55 | 56 | //Assert 57 | Assert.That(isUserAuthenticated, Is.False); 58 | } 59 | 60 | [Test] 61 | public void IsResponseFromAuthenticatedRequest_NullHttpResponseHeaders() 62 | { 63 | //Arrange 64 | HttpResponseHeaders? nullHttpResponseHeaders = null; 65 | 66 | //Act 67 | 68 | //Assert 69 | #pragma warning disable CS8604 // Possible null reference argument. 70 | Assert.That(() => GitHubApiStatusService.IsResponseFromAuthenticatedRequest(nullHttpResponseHeaders), Throws.TypeOf()); 71 | #pragma warning restore CS8604 // Possible null reference argument. 72 | } 73 | } 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/GitHubApiStatus.UnitTests/Tests/ProductHeaderValueTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using GitStatus.Common; 3 | using NUnit.Framework; 4 | 5 | namespace GitHubApiStatus.UnitTests; 6 | 7 | class AddProductHeaderValueTests : BaseTest 8 | { 9 | [Test] 10 | public void NullProductHeaderValueTest() 11 | { 12 | //Arrange 13 | var gitHubApiStatusService = new GitHubApiStatusService(); 14 | 15 | //Act 16 | 17 | //Assert 18 | #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. 19 | Assert.Multiple(() => 20 | { 21 | Assert.That(() => gitHubApiStatusService.AddProductHeaderValue(null), Throws.TypeOf()); 22 | Assert.That(gitHubApiStatusService.IsProductHeaderValueValid, Is.False); 23 | }); 24 | #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. 25 | } 26 | 27 | [Test] 28 | public void NullNameTest() 29 | { 30 | //Arrange 31 | var gitHubApiStatusService = new GitHubApiStatusService(); 32 | 33 | //Act 34 | 35 | //Assert 36 | #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. 37 | #if NET8_0_OR_GREATER 38 | Assert.Multiple(() => 39 | { 40 | Assert.That(() => gitHubApiStatusService.AddProductHeaderValue(new ProductHeaderValue(null)), Throws.TypeOf()); 41 | Assert.That(gitHubApiStatusService.IsProductHeaderValueValid, Is.False); 42 | }); 43 | #else 44 | Assert.Multiple(() => 45 | { 46 | Assert.That(() => gitHubApiStatusService.AddProductHeaderValue(new ProductHeaderValue(null)), Throws.TypeOf()); 47 | Assert.That(gitHubApiStatusService.IsProductHeaderValueValid, Is.False); 48 | }); 49 | #endif 50 | #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. 51 | } 52 | 53 | [Test] 54 | public async Task ValidProductHeaderValueTest() 55 | { 56 | //Arrange 57 | var gitHubApiStatusService = new GitHubApiStatusService(); 58 | gitHubApiStatusService.AddProductHeaderValue(new ProductHeaderValue(nameof(GitHubApiStatus))); 59 | gitHubApiStatusService.SetAuthenticationHeaderValue(new AuthenticationHeaderValue(GitHubConstants.AuthScheme, GitHubConstants.PersonalAccessToken)); 60 | 61 | //Act 62 | var apiRateLimits = await gitHubApiStatusService.GetApiRateLimits().ConfigureAwait(false); 63 | 64 | //Assert 65 | Assert.Multiple(() => 66 | { 67 | Assert.That(gitHubApiStatusService, Is.Not.Null); 68 | Assert.That(gitHubApiStatusService.IsProductHeaderValueValid, Is.True); 69 | 70 | Assert.That(apiRateLimits, Is.Not.Null); 71 | Assert.That(apiRateLimits.AppManifestConfiguration, Is.Not.Null); 72 | Assert.That(apiRateLimits.CodeScanningUpload, Is.Not.Null); 73 | Assert.That(apiRateLimits.GraphQLApi, Is.Not.Null); 74 | Assert.That(apiRateLimits.RestApi, Is.Not.Null); 75 | Assert.That(apiRateLimits.SearchApi, Is.Not.Null); 76 | Assert.That(apiRateLimits.SourceImport, Is.Not.Null); 77 | }); 78 | } 79 | } 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/GitHubApiStatus.UnitTests/Tests/SetAuthenticationHeaderValueTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using GitStatus.Common; 3 | using NUnit.Framework; 4 | 5 | namespace GitHubApiStatus.UnitTests; 6 | 7 | class SetAuthenticationHeaderValueTests 8 | { 9 | [Test] 10 | public void NullAuthenticationHeaderValue() 11 | { 12 | //Arrange 13 | var gitHubApiStatusService = new GitHubApiStatusService(); 14 | 15 | //Act 16 | 17 | //Assert 18 | Assert.Multiple(() => 19 | { 20 | #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. 21 | Assert.That(() => gitHubApiStatusService.SetAuthenticationHeaderValue(null), Throws.TypeOf()); 22 | #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. 23 | Assert.That(gitHubApiStatusService.IsAuthenticationHeaderValueSet, Is.False); 24 | }); 25 | } 26 | 27 | [TestCase("Basic")] 28 | [TestCase("Oauth")] 29 | [TestCase("Digest")] 30 | public void InvalidSchemeAuthenticationHeaderValue(string scheme) 31 | { 32 | //Arrange 33 | var gitHubApiStatusService = new GitHubApiStatusService(); 34 | var authenticationHeaderValue = new AuthenticationHeaderValue(scheme, GitHubConstants.PersonalAccessToken); 35 | 36 | //Act 37 | 38 | //Assert 39 | Assert.That(() => gitHubApiStatusService.SetAuthenticationHeaderValue(authenticationHeaderValue), Throws.TypeOf()); 40 | } 41 | 42 | [TestCase(null)] 43 | [TestCase("")] 44 | [TestCase(" ")] 45 | public void InvalidParameterAuthenticationHeaderValue(string? parameter) 46 | { 47 | //Arrange 48 | var gitHubApiStatusService = new GitHubApiStatusService(); 49 | var authenticationHeaderValue = new AuthenticationHeaderValue(GitHubConstants.AuthScheme, parameter); 50 | 51 | //Act 52 | 53 | //Assert 54 | Assert.That(() => gitHubApiStatusService.SetAuthenticationHeaderValue(authenticationHeaderValue), Throws.TypeOf()); 55 | } 56 | 57 | [Test] 58 | public async Task BEARERSchemeAuthenticationHeaderValue() 59 | { 60 | //Arrange 61 | var productHeaderValue = new ProductHeaderValue(nameof(GitHubApiStatus)); 62 | var authenticationHeaderValue = new AuthenticationHeaderValue(GitHubConstants.AuthScheme.ToUpper(), GitHubConstants.PersonalAccessToken); 63 | var gitHubApiStatusService = new GitHubApiStatusService(); 64 | 65 | //Act 66 | gitHubApiStatusService.SetAuthenticationHeaderValue(authenticationHeaderValue); 67 | gitHubApiStatusService.AddProductHeaderValue(productHeaderValue); 68 | 69 | var apiRateLimits = await gitHubApiStatusService.GetApiRateLimits().ConfigureAwait(false); 70 | 71 | //Assert 72 | Assert.Multiple(() => 73 | { 74 | Assert.That(gitHubApiStatusService, Is.Not.Null); 75 | Assert.That(apiRateLimits, Is.Not.Null); 76 | Assert.That(apiRateLimits.AppManifestConfiguration, Is.Not.Null); 77 | Assert.That(apiRateLimits.CodeScanningUpload, Is.Not.Null); 78 | Assert.That(apiRateLimits.GraphQLApi, Is.Not.Null); 79 | Assert.That(apiRateLimits.RestApi, Is.Not.Null); 80 | Assert.That(apiRateLimits.SearchApi, Is.Not.Null); 81 | Assert.That(apiRateLimits.SourceImport, Is.Not.Null); 82 | }); 83 | } 84 | } 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /src/GitHubApiStatus/.editorconfig: -------------------------------------------------------------------------------- 1 | root = false 2 | 3 | [*.cs] 4 | 5 | # Missing XML comment for publicly visible type or member 6 | dotnet_diagnostic.CS1591.severity = error 7 | 8 | # String comparison preferences 9 | dotnet_diagnostic.CA1307.severity = error 10 | dotnet_diagnostic.CA1309.severity = error -------------------------------------------------------------------------------- /src/GitHubApiStatus/GitHubApiStatus.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard1.3;netstandard2.0;netstandard2.1;net8.0;net9.0 4 | true 5 | GitHubApiStatus.snk 6 | GitHubApiStatus 7 | GitHubApiStatus 8 | GitHubApiStatus 9 | true 10 | true 11 | true 12 | snupkg 13 | 14 | GitHub API Rate Limit Status 15 | 16 | Calculate the GitHub API Rate Limits for the following GitHub APIs 17 | - REST API 18 | - Seach API 19 | - GraphQL API 20 | - Source Import API 21 | - Code Scanning Upload API 22 | - App Manifest Configuration API 23 | 24 | github, git, api, rate, rate limit 25 | GitHub API Rate Limit Status 26 | 27 | GitHub API Rate Limit Status 28 | 29 | Calculate the GitHub API Rate Limits for the following GitHub APIs 30 | - REST API 31 | - Seach API 32 | - GraphQL API 33 | - Source Import API 34 | - Code Scanning Upload API 35 | - App Manifest Configuration API 36 | 37 | 38 | New In This Release: 39 | - Add .NET 9.0 40 | 41 | $(NuGetVersion) 42 | https://github.com/brminnick/GitHubApiStatus 43 | $(AssemblyName) ($(TargetFramework)) 44 | 1.0.0.0 45 | 1.0.0.0 46 | $(Version)$(VersionSuffix) 47 | Brandon Minnick 48 | Brandon Minnick 49 | en 50 | ©Copyright 2020 Brandon Minnick. All rights reserved. 51 | false 52 | $(DefineConstants); 53 | false 54 | false 55 | MIT 56 | https://github.com/brminnick/GitHubApiStatus 57 | portable 58 | Release;Debug 59 | false 60 | 61 | 62 | true 63 | portable 64 | bin\Release\netstandard1.0\GitHubApiStatus.xml 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /src/GitHubApiStatus/GitHubApiStatus.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeTraveler/GitHubApiStatus/26c5ae40ca86fabc02000ad9d569d059b66cedc2/src/GitHubApiStatus/GitHubApiStatus.snk -------------------------------------------------------------------------------- /src/GitHubApiStatus/GitHubApiStatusException.cs: -------------------------------------------------------------------------------- 1 | namespace GitHubApiStatus; 2 | 3 | /// 4 | /// Exception thrown by GitHubApiStatus 5 | /// 6 | public sealed class GitHubApiStatusException : Exception 7 | { 8 | /// 9 | /// Initialize GitHubApiStatusException 10 | /// 11 | /// 12 | internal GitHubApiStatusException(string message) : base(message) 13 | { 14 | } 15 | 16 | /// 17 | /// Initialize GitHubApiStatusException 18 | /// 19 | /// 20 | /// 21 | internal GitHubApiStatusException(string message, Exception innerException) : base(message, innerException) 22 | { 23 | } 24 | } -------------------------------------------------------------------------------- /src/GitHubApiStatus/HttpResponseHeadersExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | 3 | namespace GitHubApiStatus; 4 | 5 | /// 6 | /// Extension Methods for HttpResponseHeaders 7 | /// 8 | public static class HttpResponseHeadersExtensions 9 | { 10 | /// 11 | /// Returns whether HttpResponseHeaders Contain X-RateLimit-Limit 12 | /// 13 | /// 14 | /// 15 | public static bool DoesContainGitHubRateLimitHeader(this HttpResponseHeaders responseHeaders) => responseHeaders.Any(x => x.Key is GitHubApiStatusService.RateLimitHeader); 16 | 17 | /// 18 | /// Returns whether HttpResponseHeaders Contain X-RateLimit-Reset 19 | /// 20 | /// 21 | /// 22 | public static bool DoesContainGitHubRateLimitResetHeader(this HttpResponseHeaders responseHeaders) => responseHeaders.Any(x => x.Key is GitHubApiStatusService.RateLimitResetHeader); 23 | 24 | /// 25 | /// Returns whether HttpResponseHeaders Contain X-RateLimit-Remaining 26 | /// 27 | /// 28 | /// 29 | public static bool DoesContainGitHubRateLimitRemainingHeader(this HttpResponseHeaders responseHeaders) => responseHeaders.Any(x => x.Key is GitHubApiStatusService.RateLimitRemainingHeader); 30 | } -------------------------------------------------------------------------------- /src/GitHubApiStatus/Interfaces/IGitHubApiRateLimitResponse.cs: -------------------------------------------------------------------------------- 1 | namespace GitHubApiStatus; 2 | 3 | interface IGitHubApiRateLimitResponse 4 | { 5 | IGitHubApiRateLimits Results { get; } 6 | } -------------------------------------------------------------------------------- /src/GitHubApiStatus/Interfaces/IGitHubApiRateLimits.cs: -------------------------------------------------------------------------------- 1 | namespace GitHubApiStatus; 2 | 3 | /// 4 | /// Interface for GitHub API Rate Limits 5 | /// 6 | public interface IGitHubApiRateLimits 7 | { 8 | /// 9 | /// REST API Rate Limit Status 10 | /// 11 | IRateLimitStatus RestApi { get; } 12 | 13 | /// 14 | /// Search API Rate Limit Status 15 | /// 16 | IRateLimitStatus SearchApi { get; } 17 | 18 | /// 19 | /// GraphQL API Rate Limit Status 20 | /// 21 | IRateLimitStatus GraphQLApi { get; } 22 | 23 | /// 24 | /// Source Import API Rate Limit Status 25 | /// 26 | IRateLimitStatus SourceImport { get; } 27 | 28 | /// 29 | /// Code Scanning API Rate Limit Status 30 | /// 31 | IRateLimitStatus CodeScanningUpload { get; } 32 | 33 | /// 34 | /// App Manifest Configuration API Rate Limit Status 35 | /// 36 | IRateLimitStatus AppManifestConfiguration { get; } 37 | } -------------------------------------------------------------------------------- /src/GitHubApiStatus/Interfaces/IGitHubApiStatusService.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Net.Http.Headers; 3 | 4 | namespace GitHubApiStatus; 5 | 6 | /// 7 | /// Interface for GitHubApiStatusService 8 | /// 9 | public interface IGitHubApiStatusService : IDisposable 10 | { 11 | /// 12 | /// Determines if GitHubApiClient.DefaultRequestHeaders.UserAgent is Valid 13 | /// 14 | bool IsProductHeaderValueValid { get; } 15 | 16 | /// 17 | /// Determines if GitHubApiClient.DefaultRequestHeaders.Authorization is Valid 18 | /// 19 | bool IsAuthenticationHeaderValueSet { get; } 20 | 21 | /// 22 | /// Add ProductHeaderValue to HttpClient.DefaultRequestHeaders.UserAgent 23 | /// 24 | /// 25 | void AddProductHeaderValue(ProductHeaderValue productHeaderValue); 26 | 27 | /// 28 | /// Set HttpClient.DefaultRequestHeaders.Authorization 29 | /// 30 | /// 31 | void SetAuthenticationHeaderValue(AuthenticationHeaderValue authenticationHeaderValue); 32 | 33 | /// 34 | /// Get the API Rate Limits for the GitHub REST API, GraphQL API, Search API, Code Scanning API and App Manifest Configuration API 35 | /// 36 | /// Cancellation Token 37 | /// 38 | Task GetApiRateLimits(CancellationToken cancellationToken); 39 | 40 | /// 41 | /// Get GitHub API Rate Limit 42 | /// 43 | /// HttpResponseHeaders from GitHub API Response 44 | /// GitHub Api Rate Limit 45 | int GetRateLimit(in HttpResponseHeaders httpResponseHeaders); 46 | 47 | /// 48 | /// Get Number of GitHub API Requests Remaining 49 | /// 50 | /// HttpResponseHeaders from GitHub API Response 51 | /// Number of GitHub API Requests Remaining 52 | int GetRemainingRequestCount(in HttpResponseHeaders httpResponseHeaders); 53 | 54 | /// 55 | /// Determines Whether GitHub's Abuse Rate Limit Has Been Reached 56 | /// 57 | /// HttpResponseHeaders from GitHub API Response 58 | /// Time Remaining in Retry-After Header 59 | /// 60 | bool IsAbuseRateLimit(in HttpResponseHeaders httpResponseHeaders, 61 | #if NETSTANDARD2_1 || NET 62 | [NotNullWhen(true)] out TimeSpan? delta); 63 | #else 64 | out TimeSpan? delta); 65 | #endif 66 | 67 | /// 68 | /// Determines Whether GitHub's Maximum API Limit Has Been Reached 69 | /// 70 | /// HttpResponseHeaders from GitHub API Response 71 | /// Whether GitHub's Maximum API Limit Has Been Reached 72 | bool HasReachedMaximumApiCallLimit(in HttpResponseHeaders httpResponseHeaders); 73 | 74 | /// 75 | /// Get Time Remaining Until GitHub API Rate Limit Resets 76 | /// 77 | /// HttpResponseHeaders from GitHub API Response 78 | /// Time Remaining Until GitHub API Rate Limit Resets 79 | TimeSpan GetRateLimitTimeRemaining(in HttpResponseHeaders httpResponseHeaders); 80 | 81 | /// 82 | /// Determines Whether the Http Response Was From an Authenticated Http Request 83 | /// 84 | /// HttpResponseHeaders from GitHub API Response 85 | /// Whether the Http Response Was From an Authenticated Http Request 86 | bool IsResponseFromAuthenticatedRequest(in HttpResponseHeaders httpResponseHeaders); 87 | 88 | /// 89 | /// Get the DateTimeOffset When the GitHub API Rate Limit Will Reset 90 | /// 91 | /// HttpResponseHeaders from GitHub API Response 92 | /// DateTimeOffset When the GitHub API Rate Limit Will Reset 93 | DateTimeOffset GetRateLimitResetDateTime(in HttpResponseHeaders httpResponseHeaders); 94 | 95 | /// 96 | /// Get the Unix Epoch Seconds When the GitHub API Rate Limit Will Reset 97 | /// 98 | /// HttpResponseHeaders from GitHub API Response 99 | /// Unix Epoch Seconds When the GitHub API Rate Limit Will Reset 100 | long GetRateLimitResetDateTime_UnixEpochSeconds(in HttpResponseHeaders httpResponseHeaders); 101 | } -------------------------------------------------------------------------------- /src/GitHubApiStatus/Interfaces/IRateLimitStatus.cs: -------------------------------------------------------------------------------- 1 | namespace GitHubApiStatus; 2 | 3 | /// 4 | /// Interface for GitHub API Raite Limit Status 5 | /// 6 | public interface IRateLimitStatus 7 | { 8 | /// 9 | /// Time Remaining until Rate Limit Reset 10 | /// 11 | TimeSpan RateLimitReset_TimeRemaining { get; } 12 | 13 | /// 14 | /// GitHub API Rate Limit 15 | /// 16 | int RateLimit { get; } 17 | 18 | /// 19 | /// Remaining Request Count to GitHub API 20 | /// 21 | int RemainingRequestCount { get; } 22 | 23 | /// 24 | /// Rate Limit Reset Time Stamp in Unix Epoch Seconds 25 | /// 26 | long RateLimitReset_UnixEpochSeconds { get; } 27 | 28 | /// 29 | /// Rate Limit Reset Time Stamp 30 | /// 31 | DateTimeOffset RateLimitReset_DateTime { get; } 32 | } -------------------------------------------------------------------------------- /src/GitHubApiStatus/Models/GitHubApiRateLimits.cs: -------------------------------------------------------------------------------- 1 | namespace GitHubApiStatus; 2 | 3 | /// 4 | /// Rate Limit data for each GitHub API 5 | /// 6 | public class GitHubApiRateLimits : IGitHubApiRateLimits 7 | { 8 | /// 9 | /// Create GitHubApiRateLimits 10 | /// 11 | /// REST API 12 | /// Search API 13 | /// GraphQL API 14 | /// Source Import API 15 | /// Integration Manifest API 16 | /// Code Scanning API 17 | public GitHubApiRateLimits(RateLimitStatus core, 18 | RateLimitStatus search, 19 | RateLimitStatus graphql, 20 | RateLimitStatus source_import, 21 | RateLimitStatus integration_manifest, 22 | RateLimitStatus code_scanning_upload) 23 | { 24 | RestApi = core; 25 | SearchApi = search; 26 | GraphQLApi = graphql; 27 | SourceImport = source_import; 28 | CodeScanningUpload = code_scanning_upload; 29 | AppManifestConfiguration = integration_manifest; 30 | } 31 | 32 | /// 33 | /// REST API Rate Limit Status 34 | /// 35 | public RateLimitStatus RestApi { get; } 36 | 37 | /// 38 | /// Search API Rate Limit Status 39 | /// 40 | public RateLimitStatus SearchApi { get; } 41 | 42 | /// 43 | /// GraphQL API Rate Limit Status 44 | /// 45 | public RateLimitStatus GraphQLApi { get; } 46 | 47 | /// 48 | /// Source Import API Rate Limit Status 49 | /// 50 | public RateLimitStatus SourceImport { get; } 51 | 52 | /// 53 | /// Code Scanning API Rate Limit Status 54 | /// 55 | public RateLimitStatus CodeScanningUpload { get; } 56 | 57 | /// 58 | /// App Manifest Configuration API Rate Limit Status 59 | /// 60 | public RateLimitStatus AppManifestConfiguration { get; } 61 | 62 | IRateLimitStatus IGitHubApiRateLimits.RestApi => RestApi; 63 | IRateLimitStatus IGitHubApiRateLimits.SearchApi => SearchApi; 64 | IRateLimitStatus IGitHubApiRateLimits.GraphQLApi => GraphQLApi; 65 | IRateLimitStatus IGitHubApiRateLimits.SourceImport => SourceImport; 66 | IRateLimitStatus IGitHubApiRateLimits.CodeScanningUpload => CodeScanningUpload; 67 | IRateLimitStatus IGitHubApiRateLimits.AppManifestConfiguration => AppManifestConfiguration; 68 | } 69 | 70 | class GitHubApiRateLimitResponse : IGitHubApiRateLimitResponse 71 | { 72 | public GitHubApiRateLimitResponse(GitHubApiRateLimits resources) => Results = resources; 73 | 74 | public GitHubApiRateLimits Results { get; } 75 | 76 | IGitHubApiRateLimits IGitHubApiRateLimitResponse.Results => Results; 77 | } -------------------------------------------------------------------------------- /src/GitHubApiStatus/Models/GitHubApiRateLimitsMutable.cs: -------------------------------------------------------------------------------- 1 | #if !NETSTANDARD1_3 2 | using System; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace GitHubApiStatus; 6 | 7 | class GitHubApiRateLimitsMutable : IGitHubApiRateLimits 8 | { 9 | [JsonPropertyName("core")] 10 | public RateLimitStatusMutable? RestApi { get; set; } 11 | 12 | [JsonPropertyName("search")] 13 | public RateLimitStatusMutable? SearchApi { get; set; } 14 | 15 | [JsonPropertyName("graphql")] 16 | public RateLimitStatusMutable? GraphQLApi { get; set; } 17 | 18 | [JsonPropertyName("source_import")] 19 | public RateLimitStatusMutable? SourceImport { get; set; } 20 | 21 | [JsonPropertyName("code_scanning_upload")] 22 | public RateLimitStatusMutable? CodeScanningUpload { get; set; } 23 | 24 | [JsonPropertyName("integration_manifest")] 25 | public RateLimitStatusMutable? AppManifestConfiguration { get; set; } 26 | 27 | IRateLimitStatus IGitHubApiRateLimits.RestApi => RestApi ?? throw new NullReferenceException(); 28 | IRateLimitStatus IGitHubApiRateLimits.SearchApi => SearchApi ?? throw new NullReferenceException(); 29 | IRateLimitStatus IGitHubApiRateLimits.GraphQLApi => GraphQLApi ?? throw new NullReferenceException(); 30 | IRateLimitStatus IGitHubApiRateLimits.SourceImport => SourceImport ?? throw new NullReferenceException(); 31 | IRateLimitStatus IGitHubApiRateLimits.CodeScanningUpload => CodeScanningUpload ?? throw new NullReferenceException(); 32 | IRateLimitStatus IGitHubApiRateLimits.AppManifestConfiguration => AppManifestConfiguration ?? throw new NullReferenceException(); 33 | 34 | public GitHubApiRateLimits ToGitHubApiRateLimits() 35 | { 36 | return new GitHubApiRateLimits(RestApi?.ToRateLimitStatus() ?? throw new NullReferenceException(), 37 | SearchApi?.ToRateLimitStatus() ?? throw new NullReferenceException(), 38 | GraphQLApi?.ToRateLimitStatus() ?? throw new NullReferenceException(), 39 | SourceImport?.ToRateLimitStatus() ?? throw new NullReferenceException(), 40 | AppManifestConfiguration?.ToRateLimitStatus() ?? throw new NullReferenceException(), 41 | CodeScanningUpload?.ToRateLimitStatus() ?? throw new NullReferenceException()); 42 | } 43 | } 44 | 45 | class RateLimitStatusMutable : IRateLimitStatus 46 | { 47 | public RateLimitStatusMutable() 48 | { 49 | RateLimitReset_DateTime = DateTimeOffset.FromUnixTimeSeconds(RateLimitReset_UnixEpochSeconds); 50 | } 51 | 52 | public TimeSpan RateLimitReset_TimeRemaining => RateLimitReset_DateTime.Subtract(DateTimeOffset.UtcNow); 53 | 54 | public DateTimeOffset RateLimitReset_DateTime { get; } 55 | 56 | [JsonPropertyName("limit")] 57 | public int RateLimit { get; set; } 58 | 59 | [JsonPropertyName("remaining")] 60 | public int RemainingRequestCount { get; set; } 61 | 62 | [JsonPropertyName("reset")] 63 | public long RateLimitReset_UnixEpochSeconds { get; set; } 64 | 65 | public RateLimitStatus ToRateLimitStatus() => new(RateLimit, RemainingRequestCount, RateLimitReset_UnixEpochSeconds); 66 | 67 | } 68 | 69 | class GitHubApiRateLimitResponseMutable : IGitHubApiRateLimitResponse 70 | { 71 | [JsonPropertyName("resources")] 72 | public GitHubApiRateLimitsMutable? Results { get; set; } 73 | 74 | IGitHubApiRateLimits IGitHubApiRateLimitResponse.Results => Results ?? throw new NullReferenceException(); 75 | 76 | public GitHubApiRateLimitResponse ToGitHubApiRateLimitResponse() => new GitHubApiRateLimitResponse(Results?.ToGitHubApiRateLimits() ?? throw new NullReferenceException()); 77 | } 78 | #endif -------------------------------------------------------------------------------- /src/GitHubApiStatus/Models/RateLimitStatus.cs: -------------------------------------------------------------------------------- 1 | namespace GitHubApiStatus; 2 | 3 | /// 4 | /// GitHub API Rate Limit Status 5 | /// 6 | public class RateLimitStatus : IRateLimitStatus 7 | { 8 | 9 | /// 10 | /// Create GitHub Rate Limit Status 11 | /// 12 | /// Maximum API Requests 13 | /// Remaining API Requests 14 | /// Rate Limit Reset Time Stamp in Unix Epoch Seconds 15 | public RateLimitStatus(int limit, int remaining, long reset) 16 | { 17 | RateLimit = limit; 18 | RemainingRequestCount = remaining; 19 | RateLimitReset_UnixEpochSeconds = reset; 20 | 21 | RateLimitReset_DateTime = DateTimeOffset.FromUnixTimeSeconds(reset); 22 | } 23 | 24 | /// 25 | /// Time Remaining until Rate Limit Reset 26 | /// 27 | public TimeSpan RateLimitReset_TimeRemaining => RateLimitReset_DateTime.Subtract(DateTimeOffset.UtcNow); 28 | 29 | /// 30 | /// GitHub API Rate Limit 31 | /// 32 | public int RateLimit { get; } 33 | 34 | /// 35 | /// Remaining Request Count to GitHub API 36 | /// 37 | public int RemainingRequestCount { get; } 38 | 39 | /// 40 | /// Rate Limit Reset Time Stamp in Unix Epoch Seconds 41 | /// 42 | public long RateLimitReset_UnixEpochSeconds { get; } 43 | 44 | /// 45 | /// Rate Limit Reset Time Stamp 46 | /// 47 | public DateTimeOffset RateLimitReset_DateTime { get; } 48 | 49 | /// 50 | /// Rate Limit Status To String 51 | /// 52 | /// Rate Limit Status 53 | public override string ToString() 54 | { 55 | return $@"Rate Limit: {RateLimit} 56 | Remaining Request Count: {RemainingRequestCount} 57 | Rate Limit Reset: {RateLimitReset_DateTime:dd MMMM @ HH:mm} 58 | Reset Time Remainaing: {RateLimitReset_TimeRemaining}"; 59 | } 60 | } --------------------------------------------------------------------------------