diff --git a/README.md b/README.md index c0cfef9..ebd8b83 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -.NET Client for interacting with Withing OAuth1 Api +.NET Client for interacting with Withings API v2 (OAuth 2.0) [![Build status](https://ci.appveyor.com/api/projects/status/lw9pd7gbdjgck3sq?svg=true)](https://ci.appveyor.com/project/atbyrd/withings-net) [![Coverage Status](https://coveralls.io/repos/github/atbyrd/Withings.NET/badge.svg?branch=master)](https://coveralls.io/github/atbyrd/Withings.NET?branch=master) @@ -9,57 +9,46 @@ [![NuGet](https://img.shields.io/nuget/v/Nuget.Core.svg?style=plastic)](https://www.nuget.org/packages/Withings.NET) ## USAGE -Due to external dependencies, your callback url should include a username param i.e. http://localhost:49294/api/oauth/callback/{username} -### All examples will use the Nancy Framework +### Authorization - Getting user authorization url +```csharp +var credentials = new WithingsCredentials(); +credentials.SetClientProperties("client_id", "client_secret"); +credentials.SetCallbackUrl("http://localhost:49294/callback"); -#### Authorization - Getting user authorization url +var authenticator = new Authenticator(credentials); +var url = authenticator.GetAuthorizeUrl("state", "user.info,user.metrics"); +// Redirect user to url ``` -Get["api/oauth/authorize", true] = async (nothing, ct) => -{ - var url = await authenticator.UserRequstUrl(requestToken).ConfigureAwait(true); - new JsonRespons(url, new DefaultJsonSerializer()); -} + +### Exchanging code for token +```csharp +var authResponse = await authenticator.GetAccessToken("authorization_code"); +var accessToken = authResponse.AccessToken; +``` + +### Fetching Data +```csharp +var client = new WithingsClient(); +var data = await client.GetActivityMeasures(DateTime.Now.AddDays(-1), DateTime.Now, "userid", accessToken); ``` ## CHANGE LOG +Version: 3.0.0 | +Release Date: Current | +Breaking Changes | +Migrated to OAuth 2.0. +Removed OAuth 1.0 support. +Updated project to SDK style (netstandard2.0). +Renamed `ConsumerKey`/`ConsumerSecret` to `ClientId`/`ClientSecret`. +Methods in `WithingsClient` now accept `accessToken` instead of `token` and `secret`. +Removed `AsyncOAuth` dependency. +Added `Flurl.Http` 3.x+ support. + Version: 2.1.0 | Release Date: April 03, 2017 | New Features | Get Ability To Get Body Measures -Version: 2.0.0 | -Release Date: April 03, 2017 | -Breaking API Change | -GetActivityMeasures Now Accepts DateTimes Instead of Strings for Dates - -Version: 1.1.29 | -Release Date:April 02, 2017 | -New Features | -Add Abiltity To Get Sleep Measures - -Version: 1.1.27 | -Release Date:April 02, 2017 | -New Features | -Add Abiltity To Get Workout Data - -Version: 1.1.26 | -Release Date:April 02, 2017 | -New Features | -Add Abiltity To Get Sleep Summary Over A Range Of Days - -Version: 1.1.23 | -Release Date:April 01, 2017 | -New Features | -Add Abiltity To Get Activity Measures For A Specific Day - -Version: 1.1.0 | -Release Date:April 01, 2017 | -New Features | -Add Abiltity To Get Activity Measures For A Date Range - -Version: 1.0.0 | -Release Date:March 06, 2017 | -New Features | -Complete Authorization Process +... diff --git a/UpgradeLog.htm b/UpgradeLog.htm deleted file mode 100644 index 16e99b3..0000000 Binary files a/UpgradeLog.htm and /dev/null differ diff --git a/Withings.NET/Client/Authenticator.cs b/Withings.NET/Client/Authenticator.cs index 45a8749..3000d2c 100644 --- a/Withings.NET/Client/Authenticator.cs +++ b/Withings.NET/Client/Authenticator.cs @@ -1,59 +1,139 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Security.Cryptography; -using System.Threading.Tasks; -using AsyncOAuth; -using Withings.NET.Models; - -[assembly: InternalsVisibleTo("Withings.Net.Specifications")] -namespace Withings.NET.Client -{ - public class Authenticator - { - readonly string _consumerKey; - readonly string _consumerSecret; - readonly string _callbackUrl; - - public Authenticator(WithingsCredentials credentials) - { - _consumerKey = credentials.ConsumerKey; - _consumerSecret = credentials.ConsumerSecret; - _callbackUrl = credentials.CallbackUrl; - - OAuthUtility.ComputeHash = (key, buffer) => { using (var hmac = new HMACSHA1(key)) { return hmac.ComputeHash(buffer); } }; - } - - public async Task GetRequestToken() - { - var authorizer = new OAuthAuthorizer(_consumerKey, _consumerSecret); - var parameters = new List> - { - new KeyValuePair("oauth_callback", Uri.EscapeUriString(_callbackUrl)) - }; - TokenResponse tokenResponse = await authorizer.GetRequestToken("https://oauth.withings.com/account/request_token", parameters); - return tokenResponse.Token; - } - - /// - /// GET USER REQUEST URL - /// - /// string - public string UserRequestUrl(RequestToken token) - { - var authorizer = new OAuthAuthorizer(_consumerKey, _consumerSecret); - return authorizer.BuildAuthorizeUrl("https://oauth.withings.com/account/authorize", token); - } - - /// - /// GET USER ACCESS TOKEN - /// - /// OAuth1Credentials - public async Task ExchangeRequestTokenForAccessToken(RequestToken requestToken, string oAuthVerifier) - { - var authorizer = new OAuthAuthorizer(_consumerKey, _consumerSecret); - TokenResponse accessTokenResponse = await authorizer.GetAccessToken("https://oauth.withings.com/account/access_token", requestToken, oAuthVerifier); - return accessTokenResponse.Token; - } - } -} \ No newline at end of file +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Flurl; +using Flurl.Http; +using Withings.NET.Models; + +namespace Withings.NET.Client +{ + public class Authenticator + { + readonly string _clientId; + readonly string _clientSecret; + readonly string _callbackUrl; + const string BaseUri = "https://wbsapi.withings.net/v2"; + + public Authenticator(WithingsCredentials credentials) + { + _clientId = credentials.ClientId; + _clientSecret = credentials.ClientSecret; + _callbackUrl = credentials.CallbackUrl; + } + + public string GetAuthorizeUrl(string state, string scope, string redirectUri = null) + { + var uri = "https://account.withings.com/oauth2_user/authorize2" + .SetQueryParam("response_type", "code") + .SetQueryParam("client_id", _clientId) + .SetQueryParam("scope", scope) + .SetQueryParam("state", state) + .SetQueryParam("redirect_uri", redirectUri ?? _callbackUrl); + + return uri.ToString(); + } + + public async Task GetAccessToken(string code, string redirectUri = null) + { + var nonce = await GetNonce(); + var action = "requesttoken"; + var grantType = "authorization_code"; + var rUri = redirectUri ?? _callbackUrl; + + var signature = GenerateSignature(action, _clientId, nonce); + + var response = await (BaseUri + "/oauth2") + .PostUrlEncodedAsync(new + { + action, + client_id = _clientId, + grant_type = grantType, + code, + redirect_uri = rUri, + nonce, + signature + }) + .ReceiveJson>(); + + return response.Body; + } + + public async Task RefreshAccessToken(string refreshToken) + { + var nonce = await GetNonce(); + var action = "requesttoken"; + var grantType = "refresh_token"; + + var signature = GenerateSignature(action, _clientId, nonce); + + var response = await (BaseUri + "/oauth2") + .PostUrlEncodedAsync(new + { + action, + client_id = _clientId, + grant_type = grantType, + refresh_token = refreshToken, + nonce, + signature + }) + .ReceiveJson>(); + + return response.Body; + } + + private async Task GetNonce() + { + var action = "getnonce"; + var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + var signature = GenerateSignature(action, _clientId, timestamp); + + var response = await (BaseUri + "/signature") + .PostUrlEncodedAsync(new + { + action, + client_id = _clientId, + timestamp, + signature + }) + .ReceiveJson>(); + + return response.Body.Nonce; + } + + private string GenerateSignature(string action, string clientId, object thirdParam) + { + // For getnonce: action, client_id, timestamp + // For requesttoken: action, client_id, nonce + // The documentation says "Concatenate the sorted values (alphabetically by key name)". + // In both cases, the keys are: + // 1. action + // 2. client_id + // 3. timestamp OR nonce + // alphabetically: action, client_id, nonce OR action, client_id, timestamp + + // So the order is always action, clientId, thirdParam. + + var data = $"{action},{clientId},{thirdParam}"; + + using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_clientSecret))) + { + var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(data)); + return BitConverter.ToString(hash).Replace("-", "").ToLower(); + } + } + + private class ResponseWrapper + { + public int Status { get; set; } + public T Body { get; set; } + } + + private class NonceResponse + { + public string Nonce { get; set; } + } + } +} diff --git a/Withings.NET/Client/OAuthBase.cs b/Withings.NET/Client/OAuthBase.cs deleted file mode 100644 index 2e2028e..0000000 --- a/Withings.NET/Client/OAuthBase.cs +++ /dev/null @@ -1,292 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Security.Cryptography; -using System.Text; -using RestSharp.Extensions.MonoHttp; -using static System.String; - -namespace Withings.NET.Client -{ - public class OAuthBase - { - - /// - /// Provides a predefined set of algorithms that are supported officially by the protocol - /// - public enum SignatureTypes - { - HMACSHA1, - PLAINTEXT, - RSASHA1 - } - - /// - /// Provides an internal structure to sort the query parameter - /// - protected class QueryParameter - { - public QueryParameter(string name, string value) - { - Name = name; - Value = value; - } - - public string Name { get; } - - public string Value { get; } - } - - /// - /// Comparer class used to perform the sorting of the query parameters - /// - protected class QueryParameterComparer : IComparer - { - - #region IComparer Members - - public int Compare(QueryParameter x, QueryParameter y) => x?.Name == y?.Name ? CompareOrdinal(x?.Value, y?.Value) : CompareOrdinal(x?.Name, y.Name); - - #endregion - } - - protected const string OAuthVersion = "1.0"; - protected const string OAuthParameterPrefix = "oauth_"; - protected const string OAuthConsumerKeyKey = "oauth_consumer_key"; - protected const string OAuthCallbackKey = "oauth_callback"; - protected const string OAuthVersionKey = "oauth_version"; - protected const string OAuthSignatureMethodKey = "oauth_signature_method"; - protected const string OAuthSignatureKey = "oauth_signature"; - protected const string OAuthTimestampKey = "oauth_timestamp"; - protected const string OAuthNonceKey = "oauth_nonce"; - protected const string OAuthTokenKey = "oauth_token"; - protected const string OAuthTokenSecretKey = "oauth_token_secret"; - protected const string Hmacsha1SignatureType = "HMAC-SHA1"; - protected const string PlainTextSignatureType = "PLAINTEXT"; - protected const string Rsasha1SignatureType = "RSA-SHA1"; - - protected Random Random = new Random(); - - protected string UnreservedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~"; - - /// - /// Helper function to compute a hash value - /// - /// The hashing algoirhtm used. If that algorithm needs some initialization, like HMAC and its derivatives, they should be initialized prior to passing it to this function - /// The data to hash - /// a Base64 string of the hash value - private string ComputeHash(HashAlgorithm hashAlgorithm, string data) - { - if (hashAlgorithm == null) - throw new ArgumentNullException(nameof(hashAlgorithm)); - - if (IsNullOrEmpty(data)) - throw new ArgumentNullException(nameof(data)); - - byte[] dataBuffer = Encoding.ASCII.GetBytes(data); - byte[] hashBytes = hashAlgorithm.ComputeHash(dataBuffer); - - return Convert.ToBase64String(hashBytes); - } - - /// - /// Internal function to cut out all non oauth query string parameters (all parameters not begining with "oauth_") - /// - /// The query string part of the Url - /// A list of QueryParameter each containing the parameter name and value - private List GetQueryParameters(string parameters) - { - if (parameters.StartsWith("?")) - parameters = parameters.Remove(0, 1); - var result = new List(); - if (!IsNullOrEmpty(parameters)) - { - string[] p = parameters.Split('&'); - foreach (var s in p) - { - if (!IsNullOrEmpty(s) && !s.StartsWith(OAuthParameterPrefix)) - { - if (s.IndexOf('=') > -1) - { - string[] temp = s.Split('='); - result.Add(new QueryParameter(temp[0], temp[1])); - } - else - { - result.Add(new QueryParameter(s, Empty)); - } - } - } - } - return result; - } - - /// - /// This is a different Url Encode implementation since the default .NET one outputs the percent encoding in lower case. - /// While this is not a problem with the percent encoding spec, it is used in upper case throughout OAuth - /// - /// The value to Url encode - /// Returns a Url encoded string - protected string UrlEncode(string value) - { - var result = new StringBuilder(); - foreach (var symbol in value) - { - if (UnreservedChars.IndexOf(symbol) != -1) - result.Append(symbol); - else - result.Append('%' + $"{(int) symbol:X2}"); - } - return result.ToString(); - } - - /// - /// Normalizes the request parameters according to the spec - /// - /// The list of parameters already sorted - /// a string representing the normalized parameters - protected string NormalizeRequestParameters(IList parameters) - { - var sb = new StringBuilder(); - for (var i = 0; i < parameters.Count; i++) - { - var p = parameters[i]; - sb.AppendFormat("{0}={1}", p.Name, p.Value); - - if (i < parameters.Count - 1) - sb.Append("&"); - } - return sb.ToString(); - } - - /// - /// Generate the signature base that is used to produce the signature - /// - /// The full url that needs to be signed including its non OAuth url parameters - /// The consumer key - /// The token, if available. If not available pass null or an empty string - /// The token secret, if available. If not available pass null or an empty string - /// The http method used. Must be a valid HTTP method verb (POST,GET,PUT, etc) - /// - /// The signature type. To use the default values use OAuthBase.SignatureTypes. - /// - /// - /// - /// The signature base - public string GenerateSignatureBase(Uri url, string consumerKey, string token, string tokenSecret, string httpMethod, string timeStamp, string nonce, string signatureType, out string normalizedUrl, out string normalizedRequestParameters) - { - if (token == null) - token = Empty; - if (IsNullOrEmpty(consumerKey)) - throw new ArgumentNullException(nameof(consumerKey)); - if (IsNullOrEmpty(httpMethod)) - throw new ArgumentNullException(nameof(httpMethod)); - if (IsNullOrEmpty(signatureType)) - throw new ArgumentNullException(nameof(signatureType)); - - List parameters = GetQueryParameters(url.Query); - parameters.Add(new QueryParameter(OAuthVersionKey, OAuthVersion)); - parameters.Add(new QueryParameter(OAuthNonceKey, nonce)); - parameters.Add(new QueryParameter(OAuthTimestampKey, timeStamp)); - parameters.Add(new QueryParameter(OAuthSignatureMethodKey, signatureType)); - parameters.Add(new QueryParameter(OAuthConsumerKeyKey, consumerKey)); - - if (!IsNullOrEmpty(token)) - parameters.Add(new QueryParameter(OAuthTokenKey, token)); - - parameters.Sort(new QueryParameterComparer()); - - normalizedUrl = $"{url.Scheme}://{url.Host}"; - if (!(url.Scheme == "http" && url.Port == 80 || url.Scheme == "https" && url.Port == 443)) - normalizedUrl += ":" + url.Port; - normalizedUrl += url.AbsolutePath; - normalizedRequestParameters = NormalizeRequestParameters(parameters); - - var signatureBase = new StringBuilder(); - signatureBase.AppendFormat("{0}&", httpMethod.ToUpper()); - signatureBase.AppendFormat("{0}&", UrlEncode(normalizedUrl)); - signatureBase.AppendFormat("{0}", UrlEncode(normalizedRequestParameters)); - return signatureBase.ToString(); - } - - /// - /// Generate the signature value based on the given signature base and hash algorithm - /// - /// The signature based as produced by the GenerateSignatureBase method or by any other means - /// The hash algorithm used to perform the hashing. If the hashing algorithm requires initialization or a key it should be set prior to calling this method - /// A base64 string of the hash value - public string GenerateSignatureUsingHash(string signatureBase, HashAlgorithm hash) => ComputeHash(hash, signatureBase); - - /// - /// Generates a signature using the HMAC-SHA1 algorithm - /// - /// The full url that needs to be signed including its non OAuth url parameters - /// The consumer key - /// The consumer seceret - /// The token, if available. If not available pass null or an empty string - /// The token secret, if available. If not available pass null or an empty string - /// The http method used. Must be a valid HTTP method verb (POST,GET,PUT, etc) - /// - /// - /// - /// - /// A base64 string of the hash value - public string GenerateSignature(Uri url, string consumerKey, string consumerSecret, string token, string tokenSecret, string httpMethod, string timeStamp, string nonce, out string normalizedUrl, out string normalizedRequestParameters) => GenerateSignature(url, consumerKey, consumerSecret, token, tokenSecret, httpMethod, timeStamp, nonce, SignatureTypes.HMACSHA1, out normalizedUrl, out normalizedRequestParameters); - - /// - /// Generates a signature using the specified signatureType - /// - /// The full url that needs to be signed including its non OAuth url parameters - /// The consumer key - /// The consumer seceret - /// The token, if available. If not available pass null or an empty string - /// The token secret, if available. If not available pass null or an empty string - /// The http method used. Must be a valid HTTP method verb (POST,GET,PUT, etc) - /// - /// The type of signature to use - /// - /// - /// - /// A base64 string of the hash value - public string GenerateSignature(Uri url, string consumerKey, string consumerSecret, string token, string tokenSecret, string httpMethod, string timeStamp, string nonce, SignatureTypes signatureType, out string normalizedUrl, out string normalizedRequestParameters) - { - normalizedUrl = null; - normalizedRequestParameters = null; - switch (signatureType) - { - case SignatureTypes.PLAINTEXT: - return HttpUtility.UrlEncode($"{consumerSecret}&{tokenSecret}"); - case SignatureTypes.HMACSHA1: - var signatureBase = GenerateSignatureBase(url, consumerKey, token, tokenSecret, httpMethod, timeStamp, nonce, Hmacsha1SignatureType, out normalizedUrl, out normalizedRequestParameters); - - var hmacsha1 = new HMACSHA1 - { - Key = Encoding.ASCII.GetBytes( - $"{UrlEncode(consumerSecret)}&{(IsNullOrEmpty(tokenSecret) ? "" : UrlEncode(tokenSecret))}") - }; - - return GenerateSignatureUsingHash(signatureBase, hmacsha1); - case SignatureTypes.RSASHA1: - throw new NotImplementedException(); - default: - throw new ArgumentException("Unknown signature type", nameof(signatureType)); - } - } - - /// - /// Generate the timestamp for the signature - /// - /// - public virtual string GenerateTimeStamp() - { - // Default implementation of UNIX time of the current UTC time - var ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0); - return Convert.ToInt64(ts.TotalSeconds).ToString(); - } - - /// - /// Generate a nonce - /// - /// - public virtual string GenerateNonce() => Random.Next(123400, 9999999).ToString(); - } -} \ No newline at end of file diff --git a/Withings.NET/Client/WithingsClient.cs b/Withings.NET/Client/WithingsClient.cs index 1b2be8c..7d1478e 100644 --- a/Withings.NET/Client/WithingsClient.cs +++ b/Withings.NET/Client/WithingsClient.cs @@ -1,240 +1,130 @@ -using System; -using System.Dynamic; -using System.Text; -using System.Threading.Tasks; -using Flurl; -using Flurl.Http; -using Withings.NET.Models; - -namespace Withings.NET.Client -{ - public class WithingsClient - { - readonly WithingsCredentials _credentials; - const string BaseUri = "https://wbsapi.withings.net/v2"; - - public WithingsClient(WithingsCredentials credentials) - { - _credentials = credentials; - } - - #region Get Activity Measures - - public async Task GetActivityMeasures(DateTime startDay, DateTime endDay, string userId, string token, string secret) - { - var query = BaseUri.AppendPathSegment("measure") - .SetQueryParam("action", "getactivity") - .SetQueryParam("userid", userId) - .SetQueryParam("startdateymd", $"{startDay:yyyy-MM-dd}") - .SetQueryParam("enddateymd", $"{endDay:yyyy-MM-dd}"); - var oAuth = new OAuthBase(); - string nonce = Convert.ToBase64String(new ASCIIEncoding().GetBytes(DateTime.Now.Ticks.ToString())); - string timeStamp = oAuth.GenerateTimeStamp(); - string normalizedUrl; - string parameters; - string signature = oAuth.GenerateSignature(new Uri(query), _credentials.ConsumerKey, _credentials.ConsumerSecret, - token, secret, "GET", timeStamp, nonce, - OAuthBase.SignatureTypes.HMACSHA1, out normalizedUrl, out parameters); - query.SetQueryParam("oauth_consumer_key", _credentials.ConsumerKey); - query.SetQueryParam("oauth_nonce", nonce); - query.SetQueryParam("oauth_signature", signature); - query.SetQueryParam("oauth_signature_method", "HMAC-SHA1"); - query.SetQueryParam("oauth_timestamp", timeStamp); - query.SetQueryParam("oauth_token", token); - query.SetQueryParam("oauth_version", "1.0"); - return await query.GetJsonAsync().ConfigureAwait(false); - } - - public async Task GetActivityMeasures(DateTime lastUpdate, string userId, string token, string secret) - { - var query = BaseUri.AppendPathSegment("measure") - .SetQueryParam("action", "getactivity") - .SetQueryParam("userid", userId) - .SetQueryParam("date", $"{lastUpdate:yyyy-MM-dd}"); - var oAuth = new OAuthBase(); - string nonce = Convert.ToBase64String(new ASCIIEncoding().GetBytes(DateTime.Now.Ticks.ToString())); - string timeStamp = oAuth.GenerateTimeStamp(); - string normalizedUrl; - string parameters; - string signature = oAuth.GenerateSignature(new Uri(query), _credentials.ConsumerKey, _credentials.ConsumerSecret, - token, secret, "GET", timeStamp, nonce, - OAuthBase.SignatureTypes.HMACSHA1, out normalizedUrl, out parameters); - query.SetQueryParam("oauth_consumer_key", _credentials.ConsumerKey); - query.SetQueryParam("oauth_nonce", nonce); - query.SetQueryParam("oauth_signature", signature); - query.SetQueryParam("oauth_signature_method", "HMAC-SHA1"); - query.SetQueryParam("oauth_timestamp", timeStamp); - query.SetQueryParam("oauth_token", token); - query.SetQueryParam("oauth_version", "1.0"); - - return await query.GetJsonAsync().ConfigureAwait(false); - } - - #endregion - - #region Get Sleep Measures/Summary - - public async Task GetSleepSummary(string startday, string endday, string token, string secret) - { - var query = BaseUri.AppendPathSegment("sleep") - .SetQueryParam("action", "getsummary") - .SetQueryParam("startdateymd", startday) - .SetQueryParam("enddateymd", endday); - var oAuth = new OAuthBase(); - string nonce = Convert.ToBase64String(new ASCIIEncoding().GetBytes(DateTime.Now.Ticks.ToString())); - string timeStamp = oAuth.GenerateTimeStamp(); - string normalizedUrl; - string parameters; - string signature = oAuth.GenerateSignature(new Uri(query), _credentials.ConsumerKey, _credentials.ConsumerSecret, - token, secret, "GET", timeStamp, nonce, - OAuthBase.SignatureTypes.HMACSHA1, out normalizedUrl, out parameters); - query.SetQueryParam("oauth_consumer_key", _credentials.ConsumerKey); - query.SetQueryParam("oauth_nonce", nonce); - query.SetQueryParam("oauth_signature", signature); - query.SetQueryParam("oauth_signature_method", "HMAC-SHA1"); - query.SetQueryParam("oauth_timestamp", timeStamp); - query.SetQueryParam("oauth_token", token); - query.SetQueryParam("oauth_version", "1.0"); - - return await query.GetJsonAsync().ConfigureAwait(false); - } - - public async Task GetSleepMeasures(string userid, DateTime startday, DateTime endday, string token, string secret) - { - var query = BaseUri.AppendPathSegment("sleep") - .SetQueryParam("action", "get") - .SetQueryParam("startdate", startday.ToUnixTime()) - .SetQueryParam("enddate", endday.ToUnixTime()); - var oAuth = new OAuthBase(); - string nonce = Convert.ToBase64String(new ASCIIEncoding().GetBytes(DateTime.Now.Ticks.ToString())); - string timeStamp = oAuth.GenerateTimeStamp(); - string normalizedUrl; - string parameters; - var signature = oAuth.GenerateSignature(new Uri(query), _credentials.ConsumerKey, _credentials.ConsumerSecret, - token, secret, "GET", timeStamp, nonce, - OAuthBase.SignatureTypes.HMACSHA1, out normalizedUrl, out parameters); - query.SetQueryParam("oauth_consumer_key", _credentials.ConsumerKey); - query.SetQueryParam("oauth_nonce", nonce); - query.SetQueryParam("oauth_signature", signature); - query.SetQueryParam("oauth_signature_method", "HMAC-SHA1"); - query.SetQueryParam("oauth_timestamp", timeStamp); - query.SetQueryParam("oauth_token", token); - query.SetQueryParam("oauth_version", "1.0"); - return await query.GetJsonAsync().ConfigureAwait(false); - } - - #endregion - - #region Get Workouts - - public async Task GetWorkouts(string startday, string endday, string token, string secret) - { - var query = BaseUri.AppendPathSegment("measure").SetQueryParam("action", "getworkouts") - .SetQueryParam("startdateymd", startday).SetQueryParam("enddateymd", endday); - var oAuth = new OAuthBase(); - var nonce = Convert.ToBase64String(new ASCIIEncoding().GetBytes(DateTime.Now.Ticks.ToString())); - var timeStamp = oAuth.GenerateTimeStamp(); - string normalizedUrl; - string parameters; - var signature = oAuth.GenerateSignature(new Uri(query), _credentials.ConsumerKey, _credentials.ConsumerSecret, - token, secret, "GET", timeStamp, nonce, - OAuthBase.SignatureTypes.HMACSHA1, out normalizedUrl, out parameters); - query.SetQueryParam("oauth_consumer_key", _credentials.ConsumerKey); - query.SetQueryParam("oauth_nonce", nonce); - query.SetQueryParam("oauth_signature", signature); - query.SetQueryParam("oauth_signature_method", "HMAC-SHA1"); - query.SetQueryParam("oauth_timestamp", timeStamp); - query.SetQueryParam("oauth_token", token); - query.SetQueryParam("oauth_version", "1.0"); - - return await query.GetJsonAsync().ConfigureAwait(false); - } - - #endregion - - #region Get Intraday Activity - - public async Task GetIntraDayActivity(string userId, DateTime start, DateTime end, string token, string secret) - { - var query = BaseUri.AppendPathSegment("measure") - .SetQueryParam("action", "getintradayactivity") - .SetQueryParam("userid", userId) - .SetQueryParam("startdate", start.ToUnixTime()) - .SetQueryParam("enddate", end.ToUnixTime()); - var oAuth = new OAuthBase(); - var nonce = Convert.ToBase64String(new ASCIIEncoding().GetBytes(DateTime.Now.Ticks.ToString())); - var timeStamp = oAuth.GenerateTimeStamp(); - string normalizedUrl; - string parameters; - var signature = oAuth.GenerateSignature(new Uri(query), _credentials.ConsumerKey, _credentials.ConsumerSecret, - token, secret, "GET", timeStamp, nonce, - OAuthBase.SignatureTypes.HMACSHA1, out normalizedUrl, out parameters); - query.SetQueryParam("oauth_consumer_key", _credentials.ConsumerKey); - query.SetQueryParam("oauth_nonce", nonce); - query.SetQueryParam("oauth_signature", signature); - query.SetQueryParam("oauth_signature_method", "HMAC-SHA1"); - query.SetQueryParam("oauth_timestamp", timeStamp); - query.SetQueryParam("oauth_token", token); - query.SetQueryParam("oauth_version", "1.0"); - - return await query.GetJsonAsync().ConfigureAwait(false); - } - - #endregion - - #region Get Body Measures - - public async Task GetBodyMeasures(string userid, DateTime start, DateTime end, string token, string secret) - { - var query = "https://wbsapi.withings.net".AppendPathSegment("measure") - .SetQueryParam("action", "getmeas") - .SetQueryParam("userid", userid) - .SetQueryParam("startdate", start.ToUnixTime()) - .SetQueryParam("enddate", end.ToUnixTime()); - var oAuth = new OAuthBase(); - var nonce = Convert.ToBase64String(new ASCIIEncoding().GetBytes(DateTime.Now.Ticks.ToString())); - var timeStamp = oAuth.GenerateTimeStamp(); - string normalizedUrl; - string parameters; - var signature = oAuth.GenerateSignature(new Uri(query), _credentials.ConsumerKey, _credentials.ConsumerSecret, - token, secret, "GET", timeStamp, nonce, - OAuthBase.SignatureTypes.HMACSHA1, out normalizedUrl, out parameters); - query.SetQueryParam("oauth_consumer_key", _credentials.ConsumerKey); - query.SetQueryParam("oauth_nonce", nonce); - query.SetQueryParam("oauth_signature", signature); - query.SetQueryParam("oauth_signature_method", "HMAC-SHA1"); - query.SetQueryParam("oauth_timestamp", timeStamp); - query.SetQueryParam("oauth_token", token); - query.SetQueryParam("oauth_version", "1.0"); - - return await query.GetJsonAsync().ConfigureAwait(false); - } - - public async Task GetBodyMeasures(string userid, DateTime lastupdate, string token, string secret) - { - var query = "https://wbsapi.withings.net".AppendPathSegment("measure") - .SetQueryParam("action", "getmeas") - .SetQueryParam("userid", userid) - .SetQueryParam("lastupdate", lastupdate.ToUnixTime()); - var oAuth = new OAuthBase(); - var nonce = Convert.ToBase64String(new ASCIIEncoding().GetBytes(DateTime.Now.Ticks.ToString())); - var timeStamp = oAuth.GenerateTimeStamp(); - string normalizedUrl; - string parameters; - var signature = oAuth.GenerateSignature(new Uri(query), _credentials.ConsumerKey, _credentials.ConsumerSecret, - token, secret, "GET", timeStamp, nonce, - OAuthBase.SignatureTypes.HMACSHA1, out normalizedUrl, out parameters); - query.SetQueryParam("oauth_consumer_key", _credentials.ConsumerKey); - query.SetQueryParam("oauth_nonce", nonce); - query.SetQueryParam("oauth_signature", signature); - query.SetQueryParam("oauth_signature_method", "HMAC-SHA1"); - query.SetQueryParam("oauth_timestamp", timeStamp); - query.SetQueryParam("oauth_token", token); - query.SetQueryParam("oauth_version", "1.0"); - - return await query.GetJsonAsync().ConfigureAwait(false); - } - - #endregion - } -} \ No newline at end of file +using System; +using System.Dynamic; +using System.Threading.Tasks; +using Flurl; +using Flurl.Http; + +namespace Withings.NET.Client +{ + public class WithingsClient + { + const string BaseUri = "https://wbsapi.withings.net"; + + public WithingsClient() + { + } + + #region Get Activity Measures + + public async Task GetActivityMeasures(DateTime startDay, DateTime endDay, string userId, string accessToken) + { + var query = BaseUri.AppendPathSegment("v2/measure") + .SetQueryParam("action", "getactivity") + .SetQueryParam("userid", userId) + .SetQueryParam("startdateymd", $"{startDay:yyyy-MM-dd}") + .SetQueryParam("enddateymd", $"{endDay:yyyy-MM-dd}") + .WithOAuthBearerToken(accessToken); + + return await query.GetJsonAsync().ConfigureAwait(false); + } + + public async Task GetActivityMeasures(DateTime lastUpdate, string userId, string accessToken) + { + var query = BaseUri.AppendPathSegment("v2/measure") + .SetQueryParam("action", "getactivity") + .SetQueryParam("userid", userId) + .SetQueryParam("date", $"{lastUpdate:yyyy-MM-dd}") + .WithOAuthBearerToken(accessToken); + + return await query.GetJsonAsync().ConfigureAwait(false); + } + + #endregion + + #region Get Sleep Measures/Summary + + public async Task GetSleepSummary(string startday, string endday, string accessToken) + { + var query = BaseUri.AppendPathSegment("v2/sleep") + .SetQueryParam("action", "getsummary") + .SetQueryParam("startdateymd", startday) + .SetQueryParam("enddateymd", endday) + .WithOAuthBearerToken(accessToken); + + return await query.GetJsonAsync().ConfigureAwait(false); + } + + public async Task GetSleepMeasures(string userid, DateTime startday, DateTime endday, string accessToken) + { + var query = BaseUri.AppendPathSegment("v2/sleep") + .SetQueryParam("action", "get") + .SetQueryParam("startdate", startday.ToUnixTime()) + .SetQueryParam("enddate", endday.ToUnixTime()) + .WithOAuthBearerToken(accessToken); + + return await query.GetJsonAsync().ConfigureAwait(false); + } + + #endregion + + #region Get Workouts + + public async Task GetWorkouts(string startday, string endday, string accessToken) + { + var query = BaseUri.AppendPathSegment("v2/measure") + .SetQueryParam("action", "getworkouts") + .SetQueryParam("startdateymd", startday) + .SetQueryParam("enddateymd", endday) + .WithOAuthBearerToken(accessToken); + + return await query.GetJsonAsync().ConfigureAwait(false); + } + + #endregion + + #region Get Intraday Activity + + public async Task GetIntraDayActivity(string userId, DateTime start, DateTime end, string accessToken) + { + var query = BaseUri.AppendPathSegment("v2/measure") + .SetQueryParam("action", "getintradayactivity") + .SetQueryParam("userid", userId) + .SetQueryParam("startdate", start.ToUnixTime()) + .SetQueryParam("enddate", end.ToUnixTime()) + .WithOAuthBearerToken(accessToken); + + return await query.GetJsonAsync().ConfigureAwait(false); + } + + #endregion + + #region Get Body Measures + + public async Task GetBodyMeasures(string userid, DateTime start, DateTime end, string accessToken) + { + // Original code used v1 for this endpoint + var query = BaseUri.AppendPathSegment("measure") + .SetQueryParam("action", "getmeas") + .SetQueryParam("userid", userid) + .SetQueryParam("startdate", start.ToUnixTime()) + .SetQueryParam("enddate", end.ToUnixTime()) + .WithOAuthBearerToken(accessToken); + + return await query.GetJsonAsync().ConfigureAwait(false); + } + + public async Task GetBodyMeasures(string userid, DateTime lastupdate, string accessToken) + { + // Original code used v1 for this endpoint + var query = BaseUri.AppendPathSegment("measure") + .SetQueryParam("action", "getmeas") + .SetQueryParam("userid", userid) + .SetQueryParam("lastupdate", lastupdate.ToUnixTime()) + .WithOAuthBearerToken(accessToken); + + return await query.GetJsonAsync().ConfigureAwait(false); + } + + #endregion + } +} diff --git a/Withings.NET/Models/AuthResponse.cs b/Withings.NET/Models/AuthResponse.cs new file mode 100644 index 0000000..4bd7ae9 --- /dev/null +++ b/Withings.NET/Models/AuthResponse.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; + +namespace Withings.NET.Models +{ + public class AuthResponse + { + [JsonProperty("access_token")] + public string AccessToken { get; set; } + + [JsonProperty("refresh_token")] + public string RefreshToken { get; set; } + + [JsonProperty("expires_in")] + public int ExpiresIn { get; set; } + + [JsonProperty("scope")] + public string Scope { get; set; } + + [JsonProperty("token_type")] + public string TokenType { get; set; } + + [JsonProperty("userid")] + public string UserId { get; set; } + } +} diff --git a/Withings.NET/Models/WithingsCredentials.cs b/Withings.NET/Models/WithingsCredentials.cs index 3ddf5e3..501d3c1 100644 --- a/Withings.NET/Models/WithingsCredentials.cs +++ b/Withings.NET/Models/WithingsCredentials.cs @@ -1,20 +1,20 @@ -namespace Withings.NET.Models -{ - public class WithingsCredentials - { - public string ConsumerKey { get; set; } - public string ConsumerSecret { get; set; } - public string CallbackUrl { get; set; } - - public void SetCallbackUrl(string url) - { - CallbackUrl = url; - } - - public void SetConsumerProperties(string key, string secret) - { - ConsumerKey = key; - ConsumerSecret = secret; - } - } -} +namespace Withings.NET.Models +{ + public class WithingsCredentials + { + public string ClientId { get; set; } + public string ClientSecret { get; set; } + public string CallbackUrl { get; set; } + + public void SetCallbackUrl(string url) + { + CallbackUrl = url; + } + + public void SetClientProperties(string id, string secret) + { + ClientId = id; + ClientSecret = secret; + } + } +} diff --git a/Withings.NET/Properties/AssemblyInfo.cs b/Withings.NET/Properties/AssemblyInfo.cs deleted file mode 100644 index 3458e01..0000000 --- a/Withings.NET/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Reflection; - -// Information about this assembly is defined by the following attributes. -// Change them to the values specific to your project. - -[assembly: AssemblyTitle("Withings.NET")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyTrademark("c 2017")] -[assembly: AssemblyCulture("")] -[assembly: AssemblyVersion("1.1.*")] -[assembly: AssemblyCopyright("Copyright © Antarr Byrd 2017")] -[assembly: AssemblyProduct("Withings.NET")] - -// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". -// The form "{Major}.{Minor}.*" will automatically update the build and revision, -// and "{Major}.{Minor}.{Build}.*" will update just the revision. - - -// The following attributes are used to specify the signing key for the assembly, -// if desired. See the Mono documentation for more information about signing. - -//[assembly: AssemblyDelaySign(false)] -//[assembly: AssemblyKeyFile("")] diff --git a/Withings.NET/Withings.csproj b/Withings.NET/Withings.csproj index cd1dad8..db1556b 100644 --- a/Withings.NET/Withings.csproj +++ b/Withings.NET/Withings.csproj @@ -1,108 +1,14 @@ - - - - Debug - AnyCPU - {D2E459A9-C641-40A5-9124-35A4539E90EE} - Library - Withings.NET - Withings.NET - v4.5 - - - - - - true - full - false - bin\Debug - DEBUG; - prompt - 4 - false - - - true - bin\Release - prompt - 4 - false - - - false - - - - - - - - ..\packages\AsyncOAuth.0.8.4\lib\AsyncOAuth.dll - True - - - ..\packages\Flurl.2.3.0\lib\portable40-net40+sl5+win8+wp8+wpa81\Flurl.dll - - - ..\packages\Flurl.Http.1.1.2\lib\net45\Flurl.Http.dll - - - - ..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.dll - True - - - ..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.dll - True - - - ..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.Desktop.dll - True - - - ..\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll - True - - - ..\packages\RestSharp.105.2.3\lib\net45\RestSharp.dll - - - - - - - - - - ..\packages\System.IO.FileSystem.Primitives.4.0.0\lib\dotnet\System.IO.FileSystem.Primitives.dll - - - - - - - - - - - - - - - - - - - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - \ No newline at end of file + + + + netstandard2.0 + true + + + + + + + + + diff --git a/Withings.NET/packages.config b/Withings.NET/packages.config deleted file mode 100644 index a859fab..0000000 --- a/Withings.NET/packages.config +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Withings.Specifications/AuthenticatorTests.cs b/Withings.Specifications/AuthenticatorTests.cs index 2a1f92b..d3b65bf 100644 --- a/Withings.Specifications/AuthenticatorTests.cs +++ b/Withings.Specifications/AuthenticatorTests.cs @@ -1,59 +1,116 @@ -using System; -using System.Threading.Tasks; -using AsyncOAuth; -using FluentAssertions; -using NUnit.Framework; -using Withings.NET.Client; -using Withings.NET.Models; - -namespace Withings.Specifications -{ - [TestFixture] - public class AuthenticatorTests - { - Authenticator _authenticator; - WithingsCredentials _credentials; - RequestToken _requestToken; - - [SetUp] - public async Task Init() - { - _credentials = new WithingsCredentials(); - _credentials.SetCallbackUrl(Environment.GetEnvironmentVariable("WithingsCallbackUrl")); - _credentials.SetConsumerProperties(Environment.GetEnvironmentVariable("WithingsConsumerKey"), Environment.GetEnvironmentVariable("WithingsConsumerSecret")); - _authenticator = new Authenticator(_credentials); - _requestToken = await _authenticator.GetRequestToken(); - } - - [Test] - public void RequestTokenTest() - { - _requestToken.Key.Should().NotBeNullOrEmpty(); - _requestToken.Secret.Should().NotBeNullOrEmpty(); - } - - [Test] - public void AuthorizeUrlTest() - { - var url = _authenticator.UserRequestUrl(_requestToken); - url.Should().NotBeNullOrEmpty(); - } - - [Test] - public void InvalidAuthorizeUrlTest() - { - Assert.Throws(() => _authenticator.UserRequestUrl(null)); - } - - [Test] - public void ExchangeInvalidRequestTokenForAccessTokenTest() - { - Assert.Throws(InvalidExchangeRequestForAccessToken); - } - - void InvalidExchangeRequestForAccessToken() - { - var unused = _authenticator.ExchangeRequestTokenForAccessToken(_requestToken, _requestToken.Secret).Result; - } - } -} +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Flurl.Http.Testing; +using NUnit.Framework; +using Withings.NET.Client; +using Withings.NET.Models; + +namespace Withings.Specifications +{ + [TestFixture] + public class AuthenticatorTests + { + Authenticator _authenticator; + WithingsCredentials _credentials; + HttpTest _httpTest; + + [SetUp] + public void Init() + { + _credentials = new WithingsCredentials(); + _credentials.SetCallbackUrl("http://localhost/callback"); + _credentials.SetClientProperties("test_client_id", "test_client_secret"); + _authenticator = new Authenticator(_credentials); + _httpTest = new HttpTest(); + } + + [TearDown] + public void Dispose() + { + _httpTest.Dispose(); + } + + [Test] + public void GetAuthorizeUrlTest() + { + var url = _authenticator.GetAuthorizeUrl("test_state", "user.info", "http://localhost/callback"); + url.Should().StartWith("https://account.withings.com/oauth2_user/authorize2"); + url.Should().Contain("client_id=test_client_id"); + url.Should().Contain("scope=user.info"); + url.Should().Contain("state=test_state"); + url.Should().Contain("redirect_uri=http%3A%2F%2Flocalhost%2Fcallback"); + } + + [Test] + public async Task GetAccessTokenTest() + { + // Mock getnonce response + _httpTest.RespondWithJson(new { status = 0, body = new { nonce = "test_nonce" } }); + // Mock requesttoken response + _httpTest.RespondWithJson(new + { + status = 0, + body = new + { + access_token = "test_access_token", + refresh_token = "test_refresh_token", + expires_in = 10800, + scope = "user.info", + token_type = "Bearer", + userid = "123" + } + }); + + var response = await _authenticator.GetAccessToken("test_code"); + + response.Should().NotBeNull(); + response.AccessToken.Should().Be("test_access_token"); + response.RefreshToken.Should().Be("test_refresh_token"); + response.UserId.Should().Be("123"); + + // Verify calls + _httpTest.ShouldHaveCalled("https://wbsapi.withings.net/v2/signature") + .WithVerb(System.Net.Http.HttpMethod.Post); + + _httpTest.ShouldHaveCalled("https://wbsapi.withings.net/v2/oauth2") + .WithVerb(System.Net.Http.HttpMethod.Post) + .WithRequestBody("*grant_type=authorization_code*") + .WithRequestBody("*code=test_code*") + .WithRequestBody("*nonce=test_nonce*"); + } + + [Test] + public async Task RefreshAccessTokenTest() + { + // Mock getnonce response + _httpTest.RespondWithJson(new { status = 0, body = new { nonce = "test_nonce" } }); + // Mock requesttoken response + _httpTest.RespondWithJson(new + { + status = 0, + body = new + { + access_token = "new_access_token", + refresh_token = "new_refresh_token", + expires_in = 10800, + scope = "user.info", + token_type = "Bearer", + userid = "123" + } + }); + + var response = await _authenticator.RefreshAccessToken("old_refresh_token"); + + response.Should().NotBeNull(); + response.AccessToken.Should().Be("new_access_token"); + + // Verify calls + _httpTest.ShouldHaveCalled("https://wbsapi.withings.net/v2/signature"); + + _httpTest.ShouldHaveCalled("https://wbsapi.withings.net/v2/oauth2") + .WithRequestBody("*grant_type=refresh_token*") + .WithRequestBody("*refresh_token=old_refresh_token*"); + } + } +} diff --git a/Withings.Specifications/DateTimeExtensionsTests.cs b/Withings.Specifications/DateTimeExtensionsTests.cs index e727009..e3da2a6 100644 --- a/Withings.Specifications/DateTimeExtensionsTests.cs +++ b/Withings.Specifications/DateTimeExtensionsTests.cs @@ -11,25 +11,35 @@ public class DateTimeExtensionsTests [Test] public void DoubleFromUnixTimeTest() { - ((double)1491934309).FromUnixTime().Date.Should().Equals(DateTime.Parse("04/11/2017")); + ((double)1491868800).FromUnixTime().Date.Should().Be(DateTime.Parse("04/11/2017")); } [Test] public void LongFromUnixTimeTest() { - ((long)1491934309).FromUnixTime().Date.Should().Equals(DateTime.Parse("04/11/2017")); + ((long)1491868800).FromUnixTime().Date.Should().Be(DateTime.Parse("04/11/2017")); } [Test] public void IntFromUnixTimeTest() { - 1491934309.FromUnixTime().Date.Should().Equals(DateTime.Parse("04/11/2017")); + 1491868800.FromUnixTime().Date.Should().Be(DateTime.Parse("04/11/2017")); } [Test] public void DateTimeToUnixTimeTest() { - DateTime.Parse("04/11/2017").ToUnixTime().Should().Equals(1491934309); + // Note: 04/11/2017 in UTC might depend on local time if not specified, + // but DateTime.Parse uses local time, and ToUnixTime in code uses UTC epoch. + // The code in DateTimeExtensions.cs: + // return Convert.ToInt64((date - epoch).TotalSeconds); + // epoch is UTC. + // If date is Unspecified (from Parse), subtraction treats it as same kind (or assumes local?). + + // Let's use a fixed UTC date to be sure. + var date = new DateTime(2017, 4, 11, 0, 0, 0, DateTimeKind.Utc); + // 1491868800 + date.ToUnixTime().Should().Be(1491868800); } } -} \ No newline at end of file +} diff --git a/Withings.Specifications/Properties/AssemblyInfo.cs b/Withings.Specifications/Properties/AssemblyInfo.cs deleted file mode 100644 index ba426fc..0000000 --- a/Withings.Specifications/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Withings.Specifications")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("nunit.tests")] -[assembly: AssemblyCopyright("Copyright © 2017")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Withings.Specifications/Withings.Net.Tests.csproj b/Withings.Specifications/Withings.Net.Tests.csproj index fc01502..1bc32a6 100644 --- a/Withings.Specifications/Withings.Net.Tests.csproj +++ b/Withings.Specifications/Withings.Net.Tests.csproj @@ -1,111 +1,20 @@ - - - - - Debug - AnyCPU - {8EDF4429-251A-416D-BB68-93F227191BCF} - Library - Properties - Withings.Specifications - Withings.Specifications - v4.5 - 512 - - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - ..\packages\AsyncOAuth.0.8.4\lib\AsyncOAuth.dll - True - - - ..\packages\FluentAssertions.4.19.2\lib\net45\FluentAssertions.dll - True - - - ..\packages\FluentAssertions.4.19.2\lib\net45\FluentAssertions.Core.dll - True - - - ..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.dll - True - - - ..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.dll - True - - - ..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.Desktop.dll - True - - - - - - - - - - - ..\packages\NUnit.3.7.1\lib\net45\nunit.framework.dll - - - - - - - - - - - Designer - - - Web.config - - - Web.config - - - - - {d2e459a9-c641-40a5-9124-35a4539e90ee} - Withings - - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - - \ No newline at end of file + + + + net8.0 + false + + + + + + + + + + + + + + + diff --git a/Withings.Specifications/WithingsClientTests.cs b/Withings.Specifications/WithingsClientTests.cs new file mode 100644 index 0000000..addc06c --- /dev/null +++ b/Withings.Specifications/WithingsClientTests.cs @@ -0,0 +1,52 @@ +using System; +using System.Dynamic; +using System.Threading.Tasks; +using FluentAssertions; +using Flurl.Http.Testing; +using NUnit.Framework; +using Withings.NET.Client; + +namespace Withings.Specifications +{ + [TestFixture] + public class WithingsClientTests + { + WithingsClient _client; + HttpTest _httpTest; + + [SetUp] + public void Init() + { + _client = new WithingsClient(); + _httpTest = new HttpTest(); + } + + [TearDown] + public void Dispose() + { + _httpTest.Dispose(); + } + + [Test] + public async Task GetActivityMeasuresTest() + { + _httpTest.RespondWithJson(new { status = 0, body = new { some_data = "test" } }); + + var start = DateTime.UtcNow.Date; + var end = DateTime.UtcNow.Date.AddDays(1); + + dynamic result = await _client.GetActivityMeasures(start, end, "userid", "access_token"); + + ((object)result).Should().BeOfType(); + // Verify property existence and value (JSON numbers are typically long/int64) + long status = result.status; + status.Should().Be(0); + + // Wait, RespondWithJson serializes the object. GetJsonAsync deserializes it. + // ExpandoObject will have properties matching the json. + // result is ExpandoObject. + // But checking properties on ExpandoObject directly needs casting to IDictionary or using dynamic. + // "status" and "body" should be present. + } + } +} diff --git a/Withings.Specifications/packages.config b/Withings.Specifications/packages.config deleted file mode 100644 index 0bd7604..0000000 --- a/Withings.Specifications/packages.config +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file