Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add subscription data #648

Merged
merged 1 commit into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions ci/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ excluded_packages=$excluded_packages"\|events\/moose"
excluded_packages=$excluded_packages"\|pb\|magefiles"
excluded_categories="root,link,firewall,route,file,integration"

tags="internal"

# In case 'full' was specified, do not exclude anything and run
# everything
if [ "${1:-""}" = "full" ]; then
Expand All @@ -24,6 +26,7 @@ if [ "${1:-""}" = "full" ]; then

excluded_packages="thisshouldneverexist"
excluded_categories="root,link"
tags="internal,moose"
fi

# Execute tests in all the packages except the excluded ones
Expand All @@ -37,7 +40,7 @@ mkdir -p "${WORKDIR}"/coverage/unit
export LD_LIBRARY_PATH="${WORKDIR}/bin/deps/lib/amd64/latest"

# shellcheck disable=SC2046
go test -tags internal -v -race $(go list -buildvcs=false ./... | grep -v "${excluded_packages}") \
go test -tags "$tags" -v -race $(go list -tags "$tags" -buildvcs=false ./... | grep -v "${excluded_packages}") \
-coverprofile "${WORKDIR}"/coverage.txt \
-exclude "${excluded_categories}" \
-args -test.gocoverdir="${WORKDIR}/coverage/unit"
Expand All @@ -50,5 +53,5 @@ go tool cover -func="${WORKDIR}"/coverage.txt

if [ "${1:-""}" = "full" ]; then
# "gocover-cobertura" is used for test coverage visualization in the diff view.
gocover-cobertura < "$WORKDIR"/coverage.txt > coverage.xml
GOFLAGS=-tags="${tags}" gocover-cobertura < "$WORKDIR"/coverage.txt > coverage.xml
fi
63 changes: 33 additions & 30 deletions cmd/daemon/events_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,37 +10,40 @@ import (

type dummyAnalytics struct{}

func (*dummyAnalytics) Enable() error { return nil }
func (*dummyAnalytics) Disable() error { return nil }
func (*dummyAnalytics) NotifyKillswitch(bool) error { return nil }
func (*dummyAnalytics) NotifyAutoconnect(bool) error { return nil }
func (*dummyAnalytics) NotifyDNS(events.DataDNS) error { return nil }
func (*dummyAnalytics) NotifyThreatProtectionLite(bool) error { return nil }
func (*dummyAnalytics) NotifyProtocol(config.Protocol) error { return nil }
func (*dummyAnalytics) NotifyAllowlist(events.DataAllowlist) error { return nil }
func (*dummyAnalytics) NotifyTechnology(config.Technology) error { return nil }
func (*dummyAnalytics) NotifyObfuscate(bool) error { return nil }
func (*dummyAnalytics) NotifyFirewall(bool) error { return nil }
func (*dummyAnalytics) NotifyRouting(bool) error { return nil }
func (*dummyAnalytics) NotifyNotify(bool) error { return nil }
func (*dummyAnalytics) NotifyMeshnet(bool) error { return nil }
func (*dummyAnalytics) NotifyIpv6(bool) error { return nil }
func (*dummyAnalytics) NotifyDefaults(any) error { return nil }
func (*dummyAnalytics) NotifyConnect(events.DataConnect) error { return nil }
func (*dummyAnalytics) NotifyDisconnect(events.DataDisconnect) error { return nil }
func (*dummyAnalytics) NotifyLogin(events.DataAuthorization) error { return nil }
func (*dummyAnalytics) NotifyLogout(events.DataAuthorization) error { return nil }
func (*dummyAnalytics) NotifyMFA(bool) error { return nil }
func (*dummyAnalytics) NotifyAccountCheck(core.ServicesResponse) error { return nil }
func (*dummyAnalytics) NotifyRequestAPI(events.DataRequestAPI) error { return nil }
func (*dummyAnalytics) NotifyUiItemsClick(events.UiItemsAction) error { return nil }
func (*dummyAnalytics) NotifyHeartBeat(int) error { return nil }
func (*dummyAnalytics) NotifyDeviceLocation(core.Insights) error { return nil }
func (*dummyAnalytics) NotifyLANDiscovery(bool) error { return nil }
func (*dummyAnalytics) NotifyVirtualLocation(bool) error { return nil }
func (*dummyAnalytics) NotifyPostquantumVpn(bool) error { return nil }
func (*dummyAnalytics) Enable() error { return nil }
func (*dummyAnalytics) Disable() error { return nil }
func (*dummyAnalytics) NotifyKillswitch(bool) error { return nil }
func (*dummyAnalytics) NotifyAutoconnect(bool) error { return nil }
func (*dummyAnalytics) NotifyDNS(events.DataDNS) error { return nil }
func (*dummyAnalytics) NotifyThreatProtectionLite(bool) error { return nil }
func (*dummyAnalytics) NotifyProtocol(config.Protocol) error { return nil }
func (*dummyAnalytics) NotifyAllowlist(events.DataAllowlist) error { return nil }
func (*dummyAnalytics) NotifyTechnology(config.Technology) error { return nil }
func (*dummyAnalytics) NotifyObfuscate(bool) error { return nil }
func (*dummyAnalytics) NotifyFirewall(bool) error { return nil }
func (*dummyAnalytics) NotifyRouting(bool) error { return nil }
func (*dummyAnalytics) NotifyNotify(bool) error { return nil }
func (*dummyAnalytics) NotifyMeshnet(bool) error { return nil }
func (*dummyAnalytics) NotifyIpv6(bool) error { return nil }
func (*dummyAnalytics) NotifyDefaults(any) error { return nil }
func (*dummyAnalytics) NotifyConnect(events.DataConnect) error { return nil }
func (*dummyAnalytics) NotifyDisconnect(events.DataDisconnect) error { return nil }
func (*dummyAnalytics) NotifyLogin(events.DataAuthorization) error { return nil }
func (*dummyAnalytics) NotifyLogout(events.DataAuthorization) error { return nil }
func (*dummyAnalytics) NotifyMFA(bool) error { return nil }
func (*dummyAnalytics) NotifyAccountCheck(any) error { return nil }
func (*dummyAnalytics) NotifyRequestAPI(events.DataRequestAPI) error { return nil }
func (*dummyAnalytics) NotifyUiItemsClick(events.UiItemsAction) error { return nil }
func (*dummyAnalytics) NotifyHeartBeat(int) error { return nil }
func (*dummyAnalytics) NotifyDeviceLocation(core.Insights) error { return nil }
func (*dummyAnalytics) NotifyLANDiscovery(bool) error { return nil }
func (*dummyAnalytics) NotifyVirtualLocation(bool) error { return nil }
func (*dummyAnalytics) NotifyPostquantumVpn(bool) error { return nil }

func newAnalytics(eventsDbPath string, fs *config.FilesystemConfigManager,
func newAnalytics(
eventsDbPath string,
fs *config.FilesystemConfigManager,
subAPI core.SubscriptionAPI,
version, env, id string) *dummyAnalytics {
return &dummyAnalytics{}
}
21 changes: 13 additions & 8 deletions cmd/daemon/events_moose.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"

"github.com/NordSecurity/nordvpn-linux/config"
"github.com/NordSecurity/nordvpn-linux/core"
"github.com/NordSecurity/nordvpn-linux/events/moose"
"github.com/NordSecurity/nordvpn-linux/internal"
)
Expand All @@ -15,7 +16,10 @@ var (
EventsSubdomain = ""
)

func newAnalytics(eventsDbPath string, fs *config.FilesystemConfigManager,
func newAnalytics(
eventsDbPath string,
fs *config.FilesystemConfigManager,
subAPI core.SubscriptionAPI,
ver, env, id string) *moose.Subscriber {
_ = os.Setenv("MOOSE_LOG_FILE", "Stdout")
logLevel := "error"
Expand All @@ -24,12 +28,13 @@ func newAnalytics(eventsDbPath string, fs *config.FilesystemConfigManager,
}
_ = os.Setenv("MOOSE_LOG", logLevel)
return &moose.Subscriber{
EventsDbPath: eventsDbPath,
Config: fs,
Version: ver,
Environment: env,
Domain: EventsDomain,
Subdomain: EventsSubdomain,
DeviceID: id,
EventsDbPath: eventsDbPath,
Config: fs,
Version: ver,
Environment: env,
Domain: EventsDomain,
Subdomain: EventsSubdomain,
DeviceID: id,
SubscriptionAPI: subAPI,
}
}
2 changes: 1 addition & 1 deletion cmd/daemon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ func main() {
// obfuscated machineID
deviceID := fmt.Sprintf("%x", sha256.Sum256([]byte(cfg.MachineID.String()+Salt)))

analytics := newAnalytics(eventsDbPath, fsystem, Version, Environment, deviceID)
analytics := newAnalytics(eventsDbPath, fsystem, defaultAPI, Version, Environment, deviceID)
if cfg.Analytics.Get() {
if err := analytics.Enable(); err != nil {
log.Println(internal.WarningPrefix, err)
Expand Down
32 changes: 32 additions & 0 deletions core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ type CombinedAPI interface {
CreateUser(email, password string) (*UserCreateResponse, error)
}

// SubscriptionAPI is responsible for fetching the subscription data of the user
type SubscriptionAPI interface {
// Orders returns a list of orders done by the user
Orders(token string) ([]Order, error)
// Payments returns a list of payments done by the user
Payments(token string) ([]PaymentResponse, error)
}

type DefaultAPI struct {
agent string
baseURL string
Expand Down Expand Up @@ -531,3 +539,27 @@ func (api *DefaultAPI) Logout(token string) error {

return nil
}

func (api *DefaultAPI) Orders(token string) ([]Order, error) {
return getData[[]Order](api, token, urlOrders)
}

func (api *DefaultAPI) Payments(token string) ([]PaymentResponse, error) {
return getData[[]PaymentResponse](api, token, urlPayments)
}

// getData calls a HTTP get request for the endpoints requiring authentication and returns the
// requested data.
func getData[T any](api *DefaultAPI, token string, url string) (T, error) {
var data T
resp, err := api.request(url, http.MethodGet, nil, token)
if err != nil {
return data, fmt.Errorf("executing HTTP GET request: %w", err)
}
defer resp.Body.Close()
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return data, fmt.Errorf("decoding data from JSON: %w", err)
}

return data, nil
}
67 changes: 64 additions & 3 deletions core/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (
"fmt"
"log"
"net/netip"
"strconv"
"strings"
"time"

"golang.org/x/exp/slices"

Expand Down Expand Up @@ -142,12 +144,73 @@ type CurrentUserResponse struct {
Email string `json:"email"`
}

type Order struct {
ID int `json:"id,omitempty"`
RemoteID int `json:"remote_id,omitempty"`
Status string `json:"status,omitempty"`
Plans Plans `json:"plans,omitempty"`
}

type PaymentResponse struct {
Payment Payment `json:"payment,omitempty"`
}

type Payment struct {
CreatedAt time.Time `json:"created_at,omitempty"`
Subscription Subscription `json:"subscription,omitempty"`
Status string `json:"status,omitempty"`
Payer Payer `json:"payer,omitempty"`
Amount float32 `json:"amount,omitempty"`
Currency string `json:"currency,omitempty"`
Provider string `json:"provider,omitempty"`
}

func (p *Payment) UnmarshalJSON(data []byte) error {
type Alias Payment
aux := &struct {
CreatedAt string `json:"created_at"`
Amount string `json:"amount"`
*Alias
}{
Alias: (*Alias)(p),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
t, err := time.Parse(internal.ServerDateFormat, aux.CreatedAt)
if err != nil {
return fmt.Errorf("parsing created_at: %w", err)
}

amount, err := strconv.ParseFloat(aux.Amount, 32)
if err != nil {
return fmt.Errorf("parsing amount: %w", err)
}

p.CreatedAt = t
p.Amount = float32(amount)
return nil
}

type Subscription struct {
MerchantID int32 `json:"merchant_id,omitempty"`
FrequencyInterval int32 `json:"frequency_interval,omitempty"`
FrequencyUnit string `json:"frequency_unit,omitempty"`
Status string `json:"status,omitempty"`
}

type Payer struct {
OrderID int `json:"order_id,omitempty"`
}

type TokenRenewResponse struct {
Token string `json:"token"`
RenewToken string `json:"renew_token"`
ExpiresAt string `json:"expires_at"`
}

type Plans []Plan

type TrustedPassTokenResponse struct {
OwnerID string `json:"owner_id"`
Token string `json:"token"`
Expand All @@ -157,10 +220,8 @@ type MultifactorAuthStatusResponse struct {
Status string `json:"status"`
}

type Plans []Plan

type Plan struct {
ID int64 `json:"id"`
ID int32 `json:"id"`
Identifier string `json:"identifier"`
Type string `json:"type"`
Title string `json:"title"`
Expand Down
47 changes: 47 additions & 0 deletions core/models_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ package core
import (
"encoding/json"
"net/netip"
"reflect"
"strconv"
"strings"
"testing"
"time"

"github.com/NordSecurity/nordvpn-linux/config"
"github.com/NordSecurity/nordvpn-linux/test/category"
Expand Down Expand Up @@ -724,3 +727,47 @@ func TestLocationsCountry(t *testing.T) {
assert.EqualValues(t, first, country)
})
}

func TestPayment_UnmarshalJSON(t *testing.T) {
category.Set(t, category.Unit)
for _, tt := range []struct {
name string
json string
payment Payment
errType error
}{
{
name: "valid payment",
json: `{"created_at": "2001-01-01 00:00:00", "amount": "1.23", "status": "done"}`,
payment: Payment{
CreatedAt: time.Date(2001, time.January, 1, 0, 0, 0, 0, time.UTC),
Amount: 1.23,
Status: "done",
},
},
{
name: "invalid JSON",
errType: &json.SyntaxError{},
},
{
name: "invalid created_at",
errType: &time.ParseError{},
json: `{"created_at": "2001-01-01"}`,
},
{
name: "invalid amount",
errType: strconv.ErrSyntax,
json: `{"created_at": "2001-01-01 00:00:00", "amount": "1.2.3"}`,
},
} {
t.Run(tt.name, func(t *testing.T) {
var p Payment
err := p.UnmarshalJSON([]byte(tt.json))
if tt.errType != nil {
target := reflect.New(reflect.TypeOf(tt.errType)).Interface()
assert.ErrorAs(t, err, target)
}
assert.Equal(t, tt.payment, p)
})
}
}
6 changes: 6 additions & 0 deletions core/urls.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ const (
// ServicesURL defines url to check user's current/expired services
ServicesURL = UsersURL + "/services"

// urlOrders defines URL to list user's orders
urlOrders = UsersURL + "/orders"

// urlOrders defines URL to list user's payments
urlPayments = UsersURL + "/payments"

// CredentialsURL defines url to generate openvpn credentials
CredentialsURL = ServicesURL + "/credentials"

Expand Down
Loading
Loading