Skip to content

Commit

Permalink
Merge pull request #4 from wneessen/breaches
Browse files Browse the repository at this point in the history
BreachedAccount, better HTTP handling and rate limiting
  • Loading branch information
wneessen authored Sep 22, 2021
2 parents b130145 + 8b29b60 commit 1606565
Show file tree
Hide file tree
Showing 5 changed files with 289 additions and 36 deletions.
67 changes: 39 additions & 28 deletions breach.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package hibp
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
Expand Down Expand Up @@ -100,25 +99,10 @@ func (b *BreachApi) Breaches(options ...BreachOption) ([]*Breach, *http.Response
queryParams := b.setBreachOpts(options...)
apiUrl := fmt.Sprintf("%s/breaches", BaseUrl)

hreq, err := b.hibp.HttpReq(http.MethodGet, apiUrl, queryParams)
hb, hr, err := b.hibp.HttpReqBody(http.MethodGet, apiUrl, queryParams)
if err != nil {
return nil, nil, err
}
hr, err := b.hibp.hc.Do(hreq)
if err != nil {
return nil, hr, err
}
if hr.StatusCode != 200 {
return nil, hr, fmt.Errorf("API responded with non HTTP-200: %s", hr.Status)
}
defer func() {
_ = hr.Body.Close()
}()

hb, err := io.ReadAll(hr.Body)
if err != nil {
return nil, hr, err
}

var breachList []*Breach
if err := json.Unmarshal(hb, &breachList); err != nil {
Expand All @@ -137,28 +121,51 @@ func (b *BreachApi) BreachByName(n string, options ...BreachOption) (*Breach, *h
}

apiUrl := fmt.Sprintf("%s/breach/%s", BaseUrl, n)

hreq, err := b.hibp.HttpReq(http.MethodGet, apiUrl, queryParams)
hb, hr, err := b.hibp.HttpReqBody(http.MethodGet, apiUrl, queryParams)
if err != nil {
return nil, nil, err
}
hr, err := b.hibp.hc.Do(hreq)

var breachDetails *Breach
if err := json.Unmarshal(hb, &breachDetails); err != nil {
return nil, hr, err
}

return breachDetails, hr, nil
}

// DataClasses are attribute of a record compromised in a breach. This method returns a list of strings
// with all registered data classes known to HIBP
func (b *BreachApi) DataClasses() ([]string, *http.Response, error) {
apiUrl := fmt.Sprintf("%s/dataclasses", BaseUrl)
hb, hr, err := b.hibp.HttpReqBody(http.MethodGet, apiUrl, nil)
if err != nil {
return nil, nil, err
}

var dataClasses []string
if err := json.Unmarshal(hb, &dataClasses); err != nil {
return nil, hr, err
}
if hr.StatusCode != 200 {
return nil, hr, fmt.Errorf("API responded with non HTTP-200: %s", hr.Status)

return dataClasses, hr, nil
}

// BreachedAccount returns a single breached site based on its name
func (b *BreachApi) BreachedAccount(a string, options ...BreachOption) ([]*Breach, *http.Response, error) {
queryParams := b.setBreachOpts(options...)

if a == "" {
return nil, nil, fmt.Errorf("no account id given")
}
defer func() {
_ = hr.Body.Close()
}()

hb, err := io.ReadAll(hr.Body)
apiUrl := fmt.Sprintf("%s/breachedaccount/%s", BaseUrl, a)
hb, hr, err := b.hibp.HttpReqBody(http.MethodGet, apiUrl, queryParams)
if err != nil {
return nil, hr, err
return nil, nil, err
}

var breachDetails *Breach
var breachDetails []*Breach
if err := json.Unmarshal(hb, &breachDetails); err != nil {
return nil, hr, err
}
Expand All @@ -174,6 +181,7 @@ func WithDomain(d string) BreachOption {
}

// WithoutTruncate disables the truncateResponse parameter in the breaches API
// This option only influences the BreachedAccount method
func WithoutTruncate() BreachOption {
return func(b *BreachApi) {
b.disableTrunc = true
Expand Down Expand Up @@ -217,6 +225,9 @@ func (b *BreachApi) setBreachOpts(options ...BreachOption) map[string]string {
}

for _, opt := range options {
if opt == nil {
continue
}
opt(b)
}

Expand Down
145 changes: 145 additions & 0 deletions breach_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package hibp

import (
"fmt"
"os"
"testing"
)

Expand All @@ -21,6 +23,23 @@ func TestBreaches(t *testing.T) {
}
}

// TestBreachesWithNil tests the Breaches() method of the breaches API with a nil option
func TestBreachesWithNil(t *testing.T) {
hc := New()
if hc == nil {
t.Errorf("hibp client creation failed")
return
}

breachList, _, err := hc.BreachApi.Breaches(nil)
if err != nil {
t.Error(err)
}
if breachList != nil && len(breachList) <= 0 {
t.Error("breaches list returned 0 results")
}
}

// TestBreachesWithDomain tests the Breaches() method of the breaches API for a specific domain
func TestBreachesWithDomain(t *testing.T) {
testTable := []struct {
Expand Down Expand Up @@ -133,3 +152,129 @@ func TestBreachByName(t *testing.T) {
})
}
}

// TestDataClasses tests the DataClasses() method of the breaches API
func TestDataClasses(t *testing.T) {
hc := New()
if hc == nil {
t.Errorf("hibp client creation failed")
return
}

classList, _, err := hc.BreachApi.DataClasses()
if err != nil {
t.Error(err)
}
if classList != nil && len(classList) <= 0 {
t.Error("breaches list returned 0 results")
}
}

// TestBreachedAccount tests the BreachedAccount() method of the breaches API
func TestBreachedAccount(t *testing.T) {
testTable := []struct {
testName string
accountName string
isBreached bool
moreThanOneBreach bool
}{
{"account-exists is breached once", "account-exists", true,
false},
{"multiple-breaches is breached multiple times", "multiple-breaches",
true, true},
{"opt-out is not breached", "opt-out", false, false},
}

apiKey := os.Getenv("HIBP_API_KEY")
if apiKey == "" {
t.SkipNow()
}
hc := New(WithApiKey(apiKey))
if hc == nil {
t.Error("failed to create HIBP client")
return
}

for _, tc := range testTable {
t.Run(tc.testName, func(t *testing.T) {
breachDetails, _, err := hc.BreachApi.BreachedAccount(
fmt.Sprintf("%s@hibp-integration-tests.com", tc.accountName))
if err != nil && tc.isBreached {
t.Error(err)
}

if breachDetails == nil && tc.isBreached {
t.Errorf("breach for the account %q is expected, but returned 0 results.",
tc.accountName)
}
if breachDetails != nil && !tc.isBreached {
t.Errorf("breach for the account %q is expected to be not breached, but returned breach details.",
tc.accountName)
}
if breachDetails != nil && tc.moreThanOneBreach && len(breachDetails) <= 1 {
t.Errorf("breach for the account %q is expected to be breached multiple, but returned %d breaches.",
tc.accountName, len(breachDetails))
}
if breachDetails != nil && !tc.moreThanOneBreach && len(breachDetails) > 1 {
t.Errorf("breach for the account %q is expected to be breached once, but returned %d breaches.",
tc.accountName, len(breachDetails))
}
})
}
}

// TestBreachedAccountWithoutTruncate tests the BreachedAccount() method of the breaches API with the
// truncateResponse option set to false
func TestBreachedAccountWithoutTruncate(t *testing.T) {
testTable := []struct {
testName string
accountName string
breachName string
breachDomain string
shouldFail bool
}{
{"account-exists is breached once", "account-exists", "Adobe",
"adobe.com", false},
{"multiple-breaches is breached multiple times", "multiple-breaches", "Adobe",
"adobe.com", false},
{"opt-out is not breached", "opt-out", "", "", true},
}

apiKey := os.Getenv("HIBP_API_KEY")
if apiKey == "" {
t.SkipNow()
}
hc := New(WithApiKey(apiKey), WithRateLimitNoFail())
if hc == nil {
t.Error("failed to create HIBP client")
return
}

for _, tc := range testTable {
t.Run(tc.testName, func(t *testing.T) {
breachDetails, _, err := hc.BreachApi.BreachedAccount(
fmt.Sprintf("%s@hibp-integration-tests.com", tc.accountName),
WithoutTruncate())
if err != nil && !tc.shouldFail {
t.Error(err)
return
}
if len(breachDetails) == 0 && !tc.shouldFail {
t.Errorf("breach details for account %q are expected but none were returned", tc.accountName)
return
}

if len(breachDetails) > 0 {
b := breachDetails[0]
if tc.breachName != b.Name {
t.Errorf("breach name for the account %q does not match. expected: %q, got: %q",
tc.accountName, tc.breachName, b.Name)
}
if tc.breachDomain != b.Domain {
t.Errorf("breach domain for the account %q does not match. expected: %q, got: %q",
tc.accountName, tc.breachDomain, b.Domain)
}
}
})
}
}
2 changes: 1 addition & 1 deletion examples/breaches/breach-by-name.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package main

import (
"fmt"
hibp "github.com/wneessen/go-hibp"
"github.com/wneessen/go-hibp"
)

func main() {
Expand Down
Loading

0 comments on commit 1606565

Please sign in to comment.