From 3d18ba6f526dab8bf0ce6535ae64e85daabcaafd Mon Sep 17 00:00:00 2001 From: Marcus Willock Date: Sun, 12 May 2024 00:29:03 -0400 Subject: [PATCH 1/3] I can create an in memory database and test it in my unit tests!!! --- _examples/rss-reader.go | 2 +- database/db.go | 60 +++++++++-- database/db_test.go | 155 +++++++++++++++++++++++++++ database/test_data/database/feeds.db | Bin 18190336 -> 18190336 bytes database/testing/init_test_file.db | Bin 32768 -> 32768 bytes 5 files changed, 209 insertions(+), 8 deletions(-) diff --git a/_examples/rss-reader.go b/_examples/rss-reader.go index 34ddbc1..9338c7f 100644 --- a/_examples/rss-reader.go +++ b/_examples/rss-reader.go @@ -23,7 +23,7 @@ func main() { slog.Debug("Successfully Parsed the Cli Args") if cli.GlobalConfig.DBExist() { - _, err = database.Init(database.CreateDBDns(cli.GlobalConfig.GetDBPath()), false) + _, err = database.Init(database.CreateDBDsn(cli.GlobalConfig.GetDBPath(), false), false) if err != nil { log.Fatalf("Unable to connect to DB at %s: %s", cli.GlobalConfig.GetDBPath(), err.Error()) } diff --git a/database/db.go b/database/db.go index 1799c36..9682bc7 100644 --- a/database/db.go +++ b/database/db.go @@ -21,11 +21,14 @@ func createTables(db *sql.DB) error { return nil } -func CreateDBDns(path string) string { +func CreateDBDsn(path string, inMemory bool) string { + if inMemory { + return fmt.Sprintf("file:%s?_foreign_keys=1&mode=memory", path) + } return fmt.Sprintf("file:%s?_foreign_keys=1", path) } -//Create -- Created the database +// Create -- Created the database func Create(path string) (*sql.DB, error) { //Create the database file _, err := os.Create(path) @@ -39,7 +42,7 @@ func Create(path string) (*sql.DB, error) { return db, err } -//Exist -- checks for the existance of a file +// Exist -- checks for the existance of a file func Exist(path string) bool { if _, err := os.Stat(path); err != nil { if os.IsNotExist(err) { @@ -49,10 +52,54 @@ func Exist(path string) bool { return true } -//Init -- Initializes the database. The reset param allows you to recreate the database. +type Connector interface { + Exec(string, ...any) (sql.Result, error) + Ping() error + QueryRow(query string, args ...any) *sql.Row + Close() error +} + +type Driver interface { + Open(string, string) (Connector, error) +} + +func createTablesV2(db Connector) error { + for _, sql := range sqlFiles { + _, err := db.Exec(sql) + if err != nil { + return fmt.Errorf("Creating tables failed with the following error: %w", err) + } + } + return nil +} + +// InitV2 -- Initializes the database. The reset param allows you to recreate the database. +func InitV2(driverName, dataSourceName string, reset bool, open func(string, string) (Connector, error)) (Connector, error) { + DB, err := open(driverName, dataSourceName) + if err != nil { + return DB, fmt.Errorf("Unable to Open Database: %w", err) + } + + err = DB.Ping() + if err != nil { + return DB, fmt.Errorf("Unable to ping the Database: %w", err) + } + + if reset { + //Drop all the tables and create all the tables again + err = createTablesV2(DB) + if err != nil { + return DB, err + } + } + + return DB, err +} + +// Init -- Initializes the database. The reset param allows you to recreate the database. func Init(dsn string, reset bool) (*sql.DB, error) { var err error - + //Prep the connection to the database DB, err = sql.Open(driver, dsn) if err != nil { @@ -76,8 +123,7 @@ func Init(dsn string, reset bool) (*sql.DB, error) { return DB, nil } - -//AddFeedFileData -- Adds Feed File Data to the database +// AddFeedFileData -- Adds Feed File Data to the database func AddFeedFileData(db *sql.DB, fileData []file.Data) (map[int64]file.Data, error) { var feedID int64 var tagID int64 diff --git a/database/db_test.go b/database/db_test.go index 0806c0c..b76b8c4 100644 --- a/database/db_test.go +++ b/database/db_test.go @@ -2,12 +2,167 @@ package database import ( "database/sql" + "errors" "fmt" "log" "os" + "strings" "testing" + + _ "github.com/mattn/go-sqlite3" //Sqlite3 driver ) +type rowErr struct{} + +func (r rowErr) Scan(dest any) error { + return errors.New("Row Scan Error") +} + +type connectorErr struct{} + +func (c connectorErr) Exec(query string, args ...any) (sql.Result, error) { + return nil, errors.New("Exec Error") +} + +func (c connectorErr) Ping() error { + return errors.New("Ping Error") +} + +func (c connectorErr) QueryRow(query string, args ...any) *sql.Row { + return nil +} + +func (c connectorErr) Close() error { + return errors.New("Connector failed to close") +} + +type driverOpenErr struct{} + +func (d driverOpenErr) Open(drivername, dsn string) (Connector, error) { + return nil, errors.New("Open Error") +} + +type driverPingErr struct{} + +func (d driverPingErr) Open(name, dsn string) (Connector, error) { + return connectorErr{}, nil +} + +func TestCreateTablesV2(t *testing.T) { + t.Parallel() + + tcs := []struct { + name string + conn Connector + err error + }{ + {"Exec error", connectorErr{}, errors.New("Creating tables failed with the following error: Exec Error")}, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + err := createTablesV2(tc.conn) + // TODO: Figure out success test cases + if err == nil { + t.Fatalf("Expected err to be nil, but got %s", err.Error()) + } + if err != nil { + if err.Error() != tc.err.Error() { + t.Fatalf("Got %q, but expected %q", err.Error(), tc.err.Error()) + } + } + }) + + } + +} + +func TestInitV2HappyPath(t *testing.T) { + t.Parallel() + + driverName := "sqlite3" + + tcs := []struct { + name string + dsn string + reset bool + }{ + {"In Memory DB", "file:memory.db?_foreign_keys=1&mode=memory", false}, + {"In Memory DB Create Tables", "file:memory.db?_foreign_keys=1&mode=memory", true}, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + _, err := InitV2(driverName, tc.dsn, false, func(a, b string) (Connector, error) { + conn, err := sql.Open(a, b) + if err != nil { + return conn, err + } + return conn, err + }) + if err != nil { + t.Fatalf("Exepected nil, but got %q", err.Error()) + } + + }) + } + +} + +func TestInitV2(t *testing.T) { + t.Parallel() + + tcs := []struct { + name string + driverName string + dsn string + reset bool + driver Driver + err error + }{ + {"Open Error", "driverName", "dsn", false, driverOpenErr{}, errors.New("Unable to Open Database: Open Error")}, + {"Ping Error", "driverName", "dsn", false, driverPingErr{}, errors.New("Unable to ping the Database: Ping Error")}, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + _, err := InitV2(tc.driverName, tc.dsn, tc.reset, tc.driver.Open) + + if err == nil { + t.Fatalf("Expected err to be nil, but got %s", err.Error()) + } + if err != nil { + if err.Error() != tc.err.Error() { + t.Fatalf("Got %q, but expected %q", err.Error(), tc.err.Error()) + } + } + }) + } +} + +func TestCreateDBDsn(t *testing.T) { + t.Parallel() + + tcs := []struct { + name string + path string + inMemory bool + expected string + }{ + {"File Path string", "testing.db", false, "file:testing.db?_foreign_keys=1"}, + {"In Memory string", "memory.db", true, "file:memory.db?_foreign_keys=1&mode=memory"}, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + result := CreateDBDsn(tc.path, tc.inMemory) + if strings.Compare(result, tc.expected) != 0 { + t.Fatalf("Expectd %q, but got %q", tc.expected, result) + } + }) + } +} + func createTestDB(file string) *sql.DB { testDB := fmt.Sprintf("file:%s%s", file, foreignKeySupport) diff --git a/database/test_data/database/feeds.db b/database/test_data/database/feeds.db index 53ba71af2bcc8b48a4d5f91cee9a22ee2366b173..98018acf2c7acf2960a7b39dddfc6273dec3a033 100644 GIT binary patch delta 1540 zcmX}ocT|;i9LI4l2=^kthj9T1ATCf5#jPk>NgLZy*`jw-D=TeB128_<${bW`J1SeY zD5GiF!gkrRY-hV{mu>L-+|%>?@%o(aIluRL&ONuOZDXv-EGtYYuV^i|OJs{2kt^aNPvnaN(OdKpeMLV}D2ha}=r2md05MPu5`)DM zQ7X#BP%%sl7v*Avs1PH?C{Za!i!oxX7$?Sy31Xs{Bqob0F-1%j)nb~MF18cfiyg!a zQ6qK~JBgjeE@D@)o7i2{ikV^$v8UKe>@D^Y`-=U<{-RDCAPy7qnMjR`S6SKt}alAM|oG9jslf=p56fsZC7pIET#OdM;ai%y+oGs1~ z4dPsJo;Y7zAR0xJXch~^g`!1BB*a2-k+@h~A{L2D#bU8UEEUT{tGG;DF0K&E#R{=f zTq&*+SBq=JwcD72uUZq#&6?<_n=DOWhx5aICo7@Js-mP_O+-le6 zR=CBk#WlEjZnmp;b*|RUa8<6-mAWDqcd^W_j@I^)%*JS9dRmW2B<511iD}6zJuN*^ zoz^2G5=%`_Oidms(Zm%0EyJ&>l9yji_8&8>nv^`Oniy6S!fJd_jSH%=VKt^P%ti-U zWsr>uvyov{5mY0BsywWQht;s48X8n(VO1JdL+r6J`TGY4*`P2R7-j>4kCg;f|F9|! ztD>MP461%%)i6=3(=QdDJ{+)|$u76K0)x(mZ9JHqV%6O{ZCJo-@yz7tD+1CG)a*#k^`>GaJn7 z<_+_vdCRQn}5u|=0DSAx)*hNk)_6;6n|3v>FH0JKk5EN p{fYUL;ZHArGW~J>BtMYlU)lcT_>=2T+@Cyu@+0dyy@E*ff^9ll^(6oR delta 1036 zcmW;IWqj5J7)D{`v_5&@)g}hKe0iv1`V}#+u!M9oXHC ziuZpXuJgM;Js+Od&KWhW(J{DU#MsG0MyG8^Nh+jL`bw2lOId29R{BYu)JuajN|Q88 zi}aTPGEfG|95ScOC3DMQnMa1mP?=ZelVLKyEFcTYLNZ(ymJzauEGmo1;=jNEW60AvYYHK`OhA*r|czr%RaKN>?ixn0dk-m zBnQhOa;O|8hszOiq>PoL8z<#L5wDV=haTrJngRJm5Jlk4RM zxlwMCo8=a{Rc`C%_NlWgM>TX#DR;GX&+HmnH*Hu)c~)t7xxJ;TPpMQ>Us-N%?p@VY z)$L7HeQHZJmDTNyy<>b&Ls#4pcg9_DcXY=+F)i+m`{Mq1ARdf|;^CMckHn+#SUet2 zL{B^!PsP*mOgtOU#q;q(ycjRV%kfIQ8n4Cc@kY#uH{-2%JKl+ROV delta 95 zcmZo@U}|V!njkI6%fP_E0mM+i&O1@Zn2~p5!aYu=&f?7uT;7aKorRNoxs91R^ClnS sPG;-OXA`#;ojj3SVzM)jC{t(d Date: Sun, 12 May 2024 00:34:29 -0400 Subject: [PATCH 2/3] lint error --- database/db_test.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/database/db_test.go b/database/db_test.go index b76b8c4..17ff3ae 100644 --- a/database/db_test.go +++ b/database/db_test.go @@ -12,12 +12,6 @@ import ( _ "github.com/mattn/go-sqlite3" //Sqlite3 driver ) -type rowErr struct{} - -func (r rowErr) Scan(dest any) error { - return errors.New("Row Scan Error") -} - type connectorErr struct{} func (c connectorErr) Exec(query string, args ...any) (sql.Result, error) { From 9fa42fce079360e90e8444a772dc445f75812aa2 Mon Sep 17 00:00:00 2001 From: Marcus Willock Date: Sun, 12 May 2024 18:57:17 -0400 Subject: [PATCH 3/3] Learned how to write unit tests for http calls --- database/feed.go | 33 ++++++++++++++-- database/feed_test.go | 57 +++++++++++++++++++++++++++ database/test_data/database/feeds.db | Bin 18190336 -> 18190336 bytes database/testing/init_test_file.db | Bin 32768 -> 32768 bytes 4 files changed, 86 insertions(+), 4 deletions(-) diff --git a/database/feed.go b/database/feed.go index bc0d780..76a14c8 100644 --- a/database/feed.go +++ b/database/feed.go @@ -12,7 +12,7 @@ import ( "github.com/crazcalm/go-rss-reader/file" ) -//Feed -- Data structure used to hold a feed +// Feed -- Data structure used to hold a feed type Feed struct { ID int64 URL string @@ -21,7 +21,32 @@ type Feed struct { Data *gofeed.Feed } -//GetFeedDataFromSite -- gets the feed data from the feed url and returns it + +// GetFeedDataFromSiteV2 -- gets the feed data from the feed url and returns it +func GetFeedDataFromSiteV2(url string, reader func(io.Reader) ([]byte, error)) (string, error) { + resp, err := http.Get(url) + if err != nil { + return "", fmt.Errorf("Error trying to get the raw feed data from %s: %s", url, err.Error()) + } + defer func() { + if err = resp.Body.Close(); err != nil { + err = fmt.Errorf("Errorr occurred while closing the response body: %s", err.Error()) + } + }() + + if resp.StatusCode >= 300 { + return "", fmt.Errorf("url %q returned a status code of %v", url, resp.StatusCode) + } + + body, err := reader(resp.Body) + if err != nil { + return "", fmt.Errorf("Unable to read response body: %w", err) + } + return string(body), err +} + + +// GetFeedDataFromSite -- gets the feed data from the feed url and returns it func GetFeedDataFromSite(url string) (string, error) { resp, err := http.Get(url) if err != nil { @@ -40,8 +65,8 @@ func GetFeedDataFromSite(url string) (string, error) { return string(body), err } -//NewFeed -- Used to create a new Feed. Id the id is equal to -1, then -//all of the database interactions will not happen +// NewFeed -- Used to create a new Feed. Id the id is equal to -1, then +// all of the database interactions will not happen func NewFeed(id int64, fileData file.Data) (*Feed, error) { var db *sql.DB var err error diff --git a/database/feed_test.go b/database/feed_test.go index 3a65e92..5af267f 100644 --- a/database/feed_test.go +++ b/database/feed_test.go @@ -1,10 +1,67 @@ package database import ( + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" "strings" "testing" ) +func TestGetFeedDataFromSiteV2(t *testing.T) { + t.Parallel() + + response_message := "hello world" + + good_server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, response_message) + })) + + bad_server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "TeaPot Error", 418) + })) + not_up_server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello, %s", r.Proto) + })) + + t.Cleanup(func() { + bad_server.Close() + good_server.Close() + }) + + tcs := []struct { + name string + url string + reader func(io.Reader) ([]byte, error) + err error + }{ + {"Not Okay Status Code", bad_server.URL, io.ReadAll, errors.New("returned a status code of 418")}, + {"Read Response Error", good_server.URL, func(_ io.Reader) ([]byte, error) { + return nil, errors.New("Read Error") + }, errors.New("Unable to read response body: Read Error")}, + {"No Server Running", not_up_server.URL, io.ReadAll, errors.New("Error trying to get the raw feed data from : Get \"\": unsupported protocol scheme \"\"")}, + {"Happy Path", good_server.URL, io.ReadAll, nil}, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + result, err := GetFeedDataFromSiteV2(tc.url, tc.reader) + if err != nil { + if strings.Contains(err.Error(), tc.err.Error()) == false { + t.Fatalf("Expected %q, but got %q", tc.err.Error(), err.Error()) + } + } else { + if strings.Compare(result, response_message) != 0 { + t.Fatalf("Expected %q, but got %q", result, response_message) + } + } + }) + } + +} + func TestGetFeedDataFromSite(t *testing.T) { tests := []string{ "http://www.leoville.tv/podcasts/sn.xml", diff --git a/database/test_data/database/feeds.db b/database/test_data/database/feeds.db index 98018acf2c7acf2960a7b39dddfc6273dec3a033..9dd1cfd4f75674262756f9709a384592740e59dc 100644 GIT binary patch delta 1135 zcmX}ob#&Er9EWl4#_rwS?*gMNu)Cc}*TC+=_OZJ$r!vInqM`yScA#R{n9f8|RBUG= z*kCKR*b1NT&S8JNp7S~9`~Lm&Yn(E>x-m@bm1~&PyFS-22FXcSSZtL^hSpWOLa<`bb~tC;erB43t4KScb?@87B2ITt>*2vXyKt z+sL-Eoop{V$d0m;>@2&;uCkl#E_=wHGE(-Ey=5QSSN49+Moyl2yMYtG?~4KI7_2O<`JS4iAQhLQ8l!JQ5xakA=s>6JdIIGCUP#gr~zZ zVP<$XJQto1v%(AE#qd&iIlK~H4X=gQLu+^=ycymKZ-;lnyJ2>CFU$#Z!~5Zb@L~8U zd>lRrpN4s1e)ueW9=-@)hOffc;hXSn_%196--jQ9aJNU2DvOzD_Xm6A^>q*SLAQ)*JOl(ug>rK@vFmz3I+t|{G8 Qx~KHW&8RE&%(aeP4E`LE6aWAK delta 1032 zcmW;IRhX3p7)4jG}Q2`M@dP^VaEAz?xvVbfo3rRoeFAK{evZxG@#bj}5mL+7MEGbLL(z1*!D=o5|EH5j_ zin5ZdEUU<>vYMtr93%(J zAu>u1mBZw4IYN$x ztAop(>PvMswbel#V{CI%Tig|Q$31axw8wpMe>@Nm#zXONJQ9z_V=*nJ$BcM9o`@&o zsdze`iD%=vcs^c;7vrUPIbMlZo&| zr}0^Q9$&hRR zQ)zq@>0G3yNS7kzBDF;-Me2&w7ilQcSft~&Yq7c&>0YEqk)B0*73p21Pib0nQ{U3; GG5-U%`Fytk diff --git a/database/testing/init_test_file.db b/database/testing/init_test_file.db index 6554972cf1e33c71b06303c0a91bd2d22fbda3b4..4e74a8545372e2c175dacd8dc1ae5f97db1b34a9 100644 GIT binary patch delta 88 zcmZo@U}|V!njkIM&%nUI0mM+i-ak>tn6ZCj!hKFgzs(L@K8%b$lY6;M7(FK+;!a`p mViUI(ojj3SVzM)j7^BDJD4u*q@6D%ptQi@7H?#5mDg*#{CKdMp delta 88 zcmZo@U}|V!njkG$&A`CG0mM+iUOiFAn6Y|e!hKFghs_RLK8%cZlY6;M7_BEC;!a_; mVH39&ojj3SVzM)j7^BtXD4u*q+s&tVtQi^YH?#5mDg*#sG!