diff --git a/Dockerfile b/Dockerfile index 6839c80..07c8901 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,35 @@ FROM golang +# Install redis server RUN apt-get update && apt-get install -y redis-server + +# Install and configure mysql +RUN echo 'mysql-server mysql-server/root_password password secret_password' | debconf-set-selections +RUN echo 'mysql-server mysql-server/root_password_again password secret_password' | debconf-set-selections +RUN apt-get install -y mysql-server +ADD build/my.cnf /etc/mysql/my.cnf +RUN mkdir -p /var/lib/mysql +RUN chmod -R 755 /var/lib/mysql + +# Install and get supervisord so that we can run multiple processes. RUN apt-get install -y supervisor RUN mkdir -p /var/log/supervisor +# Move local files to the docker image ADD . /go/src/github.com/GrappigPanda/notorious -COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf +COPY build/supervisord.conf /etc/supervisor/conf.d/supervisord.conf +# Install dependencies RUN go get gopkg.in/redis.v3 +RUN go get github.com/jinzhu/gorm +RUN go get github.com/go-sql-driver/mysql +RUN go get github.com/NotoriousTracker/viper + +# Build notorious RUN go install github.com/GrappigPanda/notorious +# Set the entry command CMD ["/usr/bin/supervisord"] +# Allow remote connections into notorious EXPOSE 3000 diff --git a/build/my.cnf b/build/my.cnf new file mode 100644 index 0000000..a0203f0 --- /dev/null +++ b/build/my.cnf @@ -0,0 +1,127 @@ +# +# The MySQL database server configuration file. +# +# You can copy this to one of: +# - "/etc/mysql/my.cnf" to set global options, +# - "~/.my.cnf" to set user-specific options. +# +# One can use all long options that the program supports. +# Run program with --help to get a list of available options and with +# --print-defaults to see which it would actually understand and use. +# +# For explanations see +# http://dev.mysql.com/doc/mysql/en/server-system-variables.html + +# This will be passed to all mysql clients +# It has been reported that passwords should be enclosed with ticks/quotes +# escpecially if they contain "#" chars... +# Remember to edit /etc/mysql/debian.cnf when changing the socket location. +[client] +port = 3306 +socket = /var/run/mysqld/mysqld.sock + +# Here is entries for some specific programs +# The following values assume you have at least 32M ram + +# This was formally known as [safe_mysqld]. Both versions are currently parsed. +[mysqld_safe] +socket = /var/run/mysqld/mysqld.sock +nice = 0 + +[mysqld] +# +# * Basic Settings +# +user = mysql +pid-file = /var/run/mysqld/mysqld.pid +socket = /var/run/mysqld/mysqld.sock +port = 3306 +basedir = /usr +datadir = /var/lib/mysql +tmpdir = /tmp +lc-messages-dir = /usr/share/mysql +skip-external-locking +# +# Instead of skip-networking the default is now to listen only on +# localhost which is more compatible and is not less secure. +# bind-address = 0.0.0.0 +# +# * Fine Tuning +# +key_buffer = 16M +max_allowed_packet = 16M +thread_stack = 192K +thread_cache_size = 8 +# This replaces the startup script and checks MyISAM tables if needed +# the first time they are touched +myisam-recover = BACKUP +#max_connections = 100 +#table_cache = 64 +#thread_concurrency = 10 +# +# * Query Cache Configuration +# +query_cache_limit = 1M +query_cache_size = 16M +# +# * Logging and Replication +# +# Both location gets rotated by the cronjob. +# Be aware that this log type is a performance killer. +# As of 5.1 you can enable the log at runtime! +# general_log_file = /var/log/mysql/mysql.log +# general_log = 1 +# +# Error log - should be very few entries. +# +log_error = /var/log/mysql/error.log +# +# Here you can see queries with especially long duration +#log_slow_queries = /var/log/mysql/mysql-slow.log +#long_query_time = 2 +#log-queries-not-using-indexes +# +# The following can be used as easy to replay backup logs or for replication. +# note: if you are setting up a replication slave, see README.Debian about +# other settings you may need to change. +#server-id = 1 +#log_bin = /var/log/mysql/mysql-bin.log +expire_logs_days = 10 +max_binlog_size = 100M +#binlog_do_db = include_database_name +#binlog_ignore_db = include_database_name +# +# * InnoDB +# +# InnoDB is enabled by default with a 10MB datafile in /var/lib/mysql/. +# Read the manual for more InnoDB related options. There are many! +# +# * Security Features +# +# Read the manual, too, if you want chroot! +# chroot = /var/lib/mysql/ +# +# For generating SSL certificates I recommend the OpenSSL GUI "tinyca". +# +# ssl-ca=/etc/mysql/cacert.pem +# ssl-cert=/etc/mysql/server-cert.pem +# ssl-key=/etc/mysql/server-key.pem + + + +[mysqldump] +quick +quote-names +max_allowed_packet = 16M + +[mysql] +no-auto-rehash # faster start of mysql but no tab completition + +[isamchk] +key_buffer = 16M + +# +# * IMPORTANT: Additional settings that can override those from this file! +# The files must end with '.cnf', otherwise they'll be ignored. +# +!includedir /etc/mysql/conf.d/ diff --git a/supervisord.conf b/build/supervisord.conf similarity index 78% rename from supervisord.conf rename to build/supervisord.conf index 5adfc15..02cc58b 100644 --- a/supervisord.conf +++ b/build/supervisord.conf @@ -8,3 +8,6 @@ command=/usr/bin/redis-server stdout_logfile=/dev/fd/1 stdout_logfile_maxbytes=0 command=/go/bin/notorious + +[program:mysql] +command=/usr/bin/mysqld_safe diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..08286e4 --- /dev/null +++ b/config/config.go @@ -0,0 +1,32 @@ +package config + +import ( + "github.com/NotoriousTracker/viper" +) + +type ConfigStruct struct { + MySQLHost string + MySQLPort string + MySQLUser string + MySQLPass string + MySQLDB string +} + +func LoadConfig() ConfigStruct { + viper.SetConfigName("config") + viper.SetConfigType("yaml") + viper.AddConfigPath(".") + + err := viper.ReadInConfig() + if err != nil { + panic("Failed to open config file") + } + + return ConfigStruct{ + viper.Get("MySQLHost").(string), + viper.Get("MySQLPort").(string), + viper.Get("MySQLUser").(string), + viper.Get("MySQLPass").(string), + viper.Get("MySQLDB").(string), + } +} diff --git a/database/database.go b/database/database.go new file mode 100644 index 0000000..732173a --- /dev/null +++ b/database/database.go @@ -0,0 +1,33 @@ +package db + +import ( + "fmt" + "github.com/GrappigPanda/notorious/config" + "github.com/jinzhu/gorm" + _ "github.com/jinzhu/gorm/dialects/mysql" +) + +func formatConnectString(c config.ConfigStruct) string { + return fmt.Sprintf("%s:%s@%s/%s", + c.MySQLUser, + c.MySQLPass, + c.MySQLHost, + c.MySQLDB, + ) +} + +func OpenConnection() *gorm.DB { + c := config.LoadConfig() + + db, err := gorm.Open("mysql", formatConnectString(c)) + if err != nil { + panic(err) + } + + return db +} + +func ScrapeTorrent(db *gorm.DB, infoHash string) interface{} { + var torrent Torrent + return db.Where("infoHash = ?", infoHash).Find(&torrent).Value +} diff --git a/database/schemas.go b/database/schemas.go new file mode 100644 index 0000000..4110805 --- /dev/null +++ b/database/schemas.go @@ -0,0 +1,16 @@ +package db + +import ( + "github.com/jinzhu/gorm" + _ "github.com/jinzhu/gorm/dialects/mysql" +) + +type Torrent struct { + gorm.Model + id int `gorm:"AUTO_INCREMENT, unique"` + infoHash string `gorm:"varchar(32), not null"` + name string `gorm:"not null"` + Downloaded int `gorm:"not null"` + Seeders int `gorm:"not null"` + Leechers int `gorm:"not null"` +} diff --git a/main b/main index 919ee9e..034fd79 100755 Binary files a/main and b/main differ diff --git a/main.go b/main.go index 084ed45..d493cd0 100644 --- a/main.go +++ b/main.go @@ -1,9 +1,18 @@ package main import ( - "github.com/GrappigPanda/notorious/server" + "github.com/GrappigPanda/notorious/reaper" + "github.com/GrappigPanda/notorious/server" + "time" ) func main() { - server.RunServer() + c := server.OpenClient() + _, err := c.Ping().Result() + if err != nil { + panic("No Redis instance detected. If deploying without Docker, install redis-server") + } + + go reaper.StartReapingScheduler(1 * time.Minute) + server.RunServer() } diff --git a/reaper/reaper.go b/reaper/reaper.go new file mode 100644 index 0000000..ed68859 --- /dev/null +++ b/reaper/reaper.go @@ -0,0 +1,76 @@ +package reaper + +import ( + "fmt" + "gopkg.in/redis.v3" + "strconv" + "strings" + "time" +) + +func reapInfoHash(c *redis.Client, infoHash string, out chan int) { + // Fan-out method to reap peers who have outlived their TTL. + keys, err := c.SMembers(infoHash).Result() + if err != nil { + panic(err) + } + + count := 0 + currTime := int64(time.Now().UTC().Unix()) + + for i := range keys { + if x := strings.Split(keys[i], ":"); len(x) != 3 { + c.SRem(infoHash, keys[i]) + + } else { + endTime := convertTimeToUnixTimeStamp(x[2]) + if currTime >= endTime { + c.SRem(infoHash, keys[i]) + count += 1 + } + } + } + + out <- count +} + +func convertTimeToUnixTimeStamp(time string) (endTime int64) { + endTime, err := strconv.ParseInt(time, 10, 64) + if err != nil { + panic(err) + } + + return +} + +func reapPeers() (peersReaped int) { + // Fans out each info in `keys *` from the Redis DB to the `reapInfoHash` + // function. + client := OpenClient() + + keys, err := getAllKeys(client, "*") + if err != nil { + panic(err) + } + + out := make(chan int) + for i := range keys { + go reapInfoHash(client, keys[i], out) + peersReaped += <-out + } + + return +} + +func StartReapingScheduler(waitTime time.Duration) { + // The timer which sets off the peer reaping every `waitTime` seconds. + reapedPeers := 0 + go func() { + for { + time.Sleep(waitTime) + fmt.Println("Starting peer reaper") + reapedPeers += reapPeers() + fmt.Printf("%v peers reaped total\n", reapedPeers) + } + }() +} diff --git a/reaper/reaperRedis.go b/reaper/reaperRedis.go new file mode 100644 index 0000000..458368b --- /dev/null +++ b/reaper/reaperRedis.go @@ -0,0 +1,31 @@ +package reaper + +import ( + "fmt" + "gopkg.in/redis.v3" +) + +func OpenClient() (client *redis.Client) { + // Opens a connection to the redis connection. + // TODO(ian): Add a config option for redis host:port + // TODO(ian): Add error checking here. + client = redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", + DB: 0, + }) + + return +} + +func getAllKeys(c *redis.Client, keymember string) (keys []string, err error) { + // getAllKeys gets all the keys for a specified `keymember` + + allKeys := c.Keys(keymember) + keys, err = allKeys.Result() + if err != nil { + fmt.Errorf("Failed to reap peers") + } + + return +} diff --git a/server/announce.go b/server/announce.go index d9c528b..3a22471 100644 --- a/server/announce.go +++ b/server/announce.go @@ -2,7 +2,6 @@ package server import ( "fmt" - "gopkg.in/redis.v3" "net/http" "net/url" "strconv" @@ -53,6 +52,11 @@ func (a *announceData) parseAnnounceData(req *http.Request) (err error) { } } a.event = query.Get("event") + if a.event == " " || a.event == "" { + a.event = "started" + } + + a.redisClient = OpenClient() return } @@ -71,12 +75,12 @@ func GetInt(u url.Values, key string) (ui uint64, err error) { return } -func (data *announceData) StartedEventHandler(c *redis.Client) { +func (data *announceData) StartedEventHandler() { // Called upon announce when a client starts a download or creates a new // torrent on the tracker. Adds a user to incomplete list in redis. - if !data.infoHashExists(c) { - data.createInfoHashKey(c) + if !data.infoHashExists() { + data.createInfoHashKey() } keymember := "" @@ -90,13 +94,13 @@ func (data *announceData) StartedEventHandler(c *redis.Client) { ipport = fmt.Sprintf("%s:%d", data.ip, data.port) } - RedisSetKeyVal(c, keymember, ipport) - if RedisSetKeyIfNotExists(c, keymember, ipport) { + RedisSetKeyVal(data.redisClient, keymember, ipport) + if RedisSetKeyIfNotExists(data.redisClient, keymember, ipport) { fmt.Printf("Adding host %s to %s\n", ipport, keymember) } } -func (data *announceData) StoppedEventHandler(c *redis.Client) { +func (data *announceData) StoppedEventHandler() { // Called upon announce whenever a client attempts to shut-down gracefully. // Ensures that the client is removed from complete/incomplete lists. @@ -104,48 +108,47 @@ func (data *announceData) StoppedEventHandler(c *redis.Client) { // gracefully, so we need to call the mysql backend and store the info and // remove the ipport from completed/incomplete redis kvs - if data.infoHashExists(c) { - // TODO(ian): THis is not done! - data.removeFromKVStorage(c, data.event) + if data.infoHashExists() { + data.removeFromKVStorage(data.event) } else { return } } -func (data *announceData) CompletedEventHandler(c *redis.Client) { +func (data *announceData) CompletedEventHandler() { // Called upon announce when a client finishes a download. Removes the // client from incomplete in redis and places their peer info into // complete. - if !data.infoHashExists(c) { - data.createInfoHashKey(c) + if !data.infoHashExists() { + data.createInfoHashKey() } else { - fmt.Printf("Removing host %s:%v to %s:incomplete\n", data.ip, data.port, data.info_hash) - data.removeFromKVStorage(c, "incomplete") + data.removeFromKVStorage("incomplete") } keymember := fmt.Sprintf("%s:complete", data.info_hash) // TODO(ian): DRY! ipport := fmt.Sprintf("%s:%s", data.ip, data.port) - if RedisSetKeyIfNotExists(c, keymember, ipport) { + if RedisSetKeyIfNotExists(data.redisClient, keymember, ipport) { fmt.Printf("Adding host %s to %s:complete\n", ipport, data.info_hash) } } -func (data *announceData) removeFromKVStorage(c *redis.Client, subkey string) { +func (data *announceData) removeFromKVStorage(subkey string) { // Remove the subkey from the kv storage. - - ipport := fmt.Sprintf("%s:%s", data.ip, data.port) + ipport := fmt.Sprintf("%s:%d", data.ip, data.port) keymember := fmt.Sprintf("%s:%s", data.info_hash, subkey) - RedisRemoveKeysValue(c, keymember, ipport) + + fmt.Printf("Removing host %s from %v\n", ipport, keymember) + RedisRemoveKeysValue(data.redisClient, keymember, ipport) } -func (data *announceData) infoHashExists(c *redis.Client) bool { - return RedisGetBoolKeyVal(c, data.info_hash, data) +func (data *announceData) infoHashExists() bool { + return RedisGetBoolKeyVal(data.redisClient, data.info_hash, data) } -func (data *announceData) createInfoHashKey(c *redis.Client) { - CreateNewTorrentKey(c, data.info_hash) +func (data *announceData) createInfoHashKey() { + CreateNewTorrentKey(data.redisClient, data.info_hash) } func ParseInfoHash(s string) string { diff --git a/server/announce_response.go b/server/announce_response.go index df4aa04..b0e07d9 100644 --- a/server/announce_response.go +++ b/server/announce_response.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" "github.com/GrappigPanda/notorious/bencode" - "gopkg.in/redis.v3" "net" "strconv" "strings" @@ -60,14 +59,14 @@ func CompactAllPeers(ipport []string) []byte { return ret.Bytes() } -func formatResponseData(c *redis.Client, ips []string, data *announceData) string { - return EncodeResponse(c, ips, data) +func formatResponseData(ips []string, data *announceData) string { + return EncodeResponse(ips, data) } -func EncodeResponse(c *redis.Client, ipport []string, data *announceData) (resp string) { +func EncodeResponse(ipport []string, data *announceData) (resp string) { ret := "" - completeCount := len(RedisGetKeyVal(c, data.info_hash, data)) - incompleteCount := len(RedisGetKeyVal(c, data.info_hash, data)) + completeCount := len(RedisGetKeyVal(data.redisClient, data.info_hash, data)) + incompleteCount := len(RedisGetKeyVal(data.redisClient, data.info_hash, data)) ret += bencode.EncodeKV("complete", bencode.EncodeInt(completeCount)) ret += bencode.EncodeKV("incomplete", bencode.EncodeInt(incompleteCount)) diff --git a/server/definitions.go b/server/definitions.go index 1489ab7..391fea5 100644 --- a/server/definitions.go +++ b/server/definitions.go @@ -1,5 +1,9 @@ package server +import ( + "gopkg.in/redis.v3" +) + const ( STARTED = iota COMPLETED @@ -13,16 +17,27 @@ type TorrentEvent struct { } type announceData struct { - info_hash string //20 byte sha1 hash - peer_id string //max len 20 - ip string //optional - event string // TorrentEvent - port uint64 // port number the peer is listening on - uploaded uint64 // base10 ascii amount uploaded so far - downloaded uint64 // base10 ascii amount downloaded so far - left uint64 // # of bytes left to download (base 10 ascii) - numwant uint64 // Number of peers requested by client. - compact bool // Bep23 peer list compression decision: True -> compress bep23 + info_hash string //20 byte sha1 hash + peer_id string //max len 20 + ip string //optional + event string // TorrentEvent + port uint64 // port number the peer is listening on + uploaded uint64 // base10 ascii amount uploaded so far + downloaded uint64 // base10 ascii amount downloaded so far + left uint64 // # of bytes left to download (base 10 ascii) + numwant uint64 // Number of peers requested by client. + compact bool // Bep23 peer list compression decision: True -> compress bep23 + redisClient *redis.Client // The redis client connection handler to use. +} + +type scrapeData struct { + infoHash string +} + +type scrapeResponse struct { + complete uint64 + downloaded uint64 + incomplete uint64 } type TorrentResponseData struct { @@ -35,3 +50,6 @@ type TorrentResponseData struct { } var ANNOUNCE_URL = "/announce" + +// TODO(ian): Set this expireTime to a config-loaded value. +// expireTime := 5 * 60 diff --git a/server/redis.go b/server/redis.go index 94e6f09..06364d4 100644 --- a/server/redis.go +++ b/server/redis.go @@ -4,12 +4,54 @@ import ( "bytes" "fmt" "gopkg.in/redis.v3" + "strings" + "time" ) -func RedisSetKeyVal(client *redis.Client, keymember string, value string) { +var EXPIRETIME int64 = 5 * 60 + +func OpenClient() (client *redis.Client) { + client = redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", + DB: 0, + }) + + return +} + +func RedisSetIPMember(data *announceData) (retval int) { + c := data.redisClient + + keymember := concatenateKeyMember(data.info_hash, "ip") + + currTime := int64(time.Now().UTC().AddDate(0, 0, 1).Unix()) + + ipPort := fmt.Sprintf("%s:%v", createIpPortPair(data), currTime) + + if err := c.SAdd(keymember, ipPort).Err(); err != nil { + retval = 0 + panic("Failed to add key") + + } else { + retval = 1 + } + + return +} + +func RedisSetKeyVal(c *redis.Client, keymember string, value string) { // RedisSetKeyVal sets a key:member's value to value. Returns nothing as of // yet. - client.SAdd(keymember, value) + currTime := int64(time.Now().UTC().Unix()) + currTime += EXPIRETIME + value = fmt.Sprintf("%v:%v", value, currTime) + + if sz := strings.Split(value, ":"); len(sz) >= 1 { + // If the value being added can be converted to an int, it is a ip:port key + // and we can set an expiration on it. + c.SAdd(keymember, value) + } } func RedisGetKeyVal(client *redis.Client, key string, value *announceData) []string { @@ -17,7 +59,6 @@ func RedisGetKeyVal(client *redis.Client, key string, value *announceData) []str // provided key. If the key does not yet exist, we create the key in the KV // storage or if the value is empty, we add the current requester to the // list. - // TODO(ian): Don't explicitly access at `complete` keymember := concatenateKeyMember(key, "complete") val, err := client.SMembers(keymember).Result() diff --git a/server/server.go b/server/server.go index 509d963..60ac329 100644 --- a/server/server.go +++ b/server/server.go @@ -2,56 +2,54 @@ package server import ( "fmt" - "gopkg.in/redis.v3" + "github.com/GrappigPanda/notorious/database" "net/http" ) var FIELDS = []string{"port", "uploaded", "downloaded", "left", "event", "compact"} -func worker(client *redis.Client, data *announceData) []string { - if RedisGetBoolKeyVal(client, data.info_hash, data) { - x := RedisGetKeyVal(client, data.info_hash, data) +func worker(data *announceData) []string { + if RedisGetBoolKeyVal(data.redisClient, data.info_hash, data) { + x := RedisGetKeyVal(data.redisClient, data.info_hash, data) - RedisSetKeyVal(client, - concatenateKeyMember(data.info_hash, "ip"), - createIpPortPair(data)) + RedisSetIPMember(data) return x } else { - CreateNewTorrentKey(client, data.info_hash) - return worker(client, data) + CreateNewTorrentKey(data.redisClient, data.info_hash) + return worker(data) } } func requestHandler(w http.ResponseWriter, req *http.Request) { - client := OpenClient() - data := new(announceData) err := data.parseAnnounceData(req) if err != nil { panic(err) } + fmt.Printf("Event: %s from host %s on port %v\n", data.event, data.ip, data.port) + switch data.event { + case "started": - data.event = "started" - data.StartedEventHandler(client) + data.StartedEventHandler() + case "stopped": - data.StoppedEventHandler(client) + data.StoppedEventHandler() + case "completed": - data.CompletedEventHandler(client) + data.CompletedEventHandler() default: - data.event = "started" - data.StartedEventHandler(client) + panic(fmt.Errorf("We're somehow getting this strange error...")) } - fmt.Printf("Event: %s from host %s on port %v\n", data.event, data.ip, data.port) if data.event == "started" || data.event == "completed" { - worker(client, data) - x := RedisGetKeyVal(client, data.info_hash, data) + worker(data) + x := RedisGetKeyVal(data.redisClient, data.info_hash, data) // TODO(ian): Move this into a seperate function. - // TODO(ian): Remove this magic number and use data.numwant, but limit ti + // TODO(ian): Remove this magic number and use data.numwant, but limit it // to 30 max, as that's the bittorrent protocol suggested limit. if len(x) >= 30 { x = x[0:30] @@ -61,9 +59,10 @@ func requestHandler(w http.ResponseWriter, req *http.Request) { if len(x) > 0 { w.Header().Set("Content-Type", "text/plain") - response := formatResponseData(client, x, data) + response := formatResponseData(x, data) w.Write([]byte(response)) + } else { failMsg := fmt.Sprintf("No peers for torrent %s\n", data.info_hash) w.Write([]byte(createFailureMessage(failMsg))) @@ -71,19 +70,18 @@ func requestHandler(w http.ResponseWriter, req *http.Request) { } } +func scrapeHandler(w http.ResponseWriter, req *http.Request) interface{} { + query := req.URL.Query() + infoHash := ParseInfoHash(query.Get("info_hash")) + + data := db.ScrapeTorrent(db.OpenConnection(), infoHash) + return data +} + func RunServer() { mux := http.NewServeMux() mux.HandleFunc("/announce", requestHandler) + //mux.HandleFunc("/scrape", scrapeHandler) http.ListenAndServe(":3000", mux) } - -func OpenClient() *redis.Client { - client := redis.NewClient(&redis.Options{ - Addr: "localhost:6379", - Password: "", - DB: 0, - }) - - return client -} diff --git a/server/server_test.go b/server/server_test.go index 27994cb..121d02a 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -3,6 +3,7 @@ package server import ( "fmt" "testing" + "net/url" ) func TestParseUrlQuery(t *testing.T) { @@ -51,3 +52,37 @@ func TestParseInfoHash(t *testing.T) { t.Fatalf("Expected %s, got %s", expectedResult, result) } } + +func TestGetIntFailEmptyKey(t *testing.T) { + u, _ := url.Parse("http://google.com/") + urlValues := u.Query() + key := "testInt" + + expectedResult := uint64(50) + result, err := GetInt(urlValues, key) + if err == nil { + t.Fatalf("We somehow found the key?") + } + + if result == expectedResult { + t.Fatalf("Expected %s, got %s", expectedResult, result) + } +} + +func TestGetInt(t *testing.T) { + u, _ := url.Parse("http://google.com/?testInt=50") + urlValues := u.Query() + key := "testInt" + + expectedResult := uint64(50) + result, err := GetInt(urlValues, key) + if err != nil { + t.Fatalf("Failed to GetInt() with %v", err) + } + + if result != expectedResult || err != nil { + t.Fatalf("Expected %s, got %s", expectedResult, result) + } +} + +