diff --git a/README.md b/README.md index cb49490..5b6ed40 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,44 @@ if restErr != nil { } ``` +### Get Notes + +```go +client := ankiconnect.NewClient() + +// Get the Note Ids of cards due today +nodeIds, restErr := client.Notes.Get("prop:due=0") +if restErr != nil { + log.Fatal(restErr) +} + +// Get the Note data of cards due today +notes, restErr := client.Notes.Get("prop:due=0") +if restErr != nil { + log.Fatal(restErr) +} + +``` + +### Get Cards + +```go +client := ankiconnect.NewClient() + +// Get the Card Ids of cards due today +nodeIds, restErr := client.Cards.Get("prop:due=0") +if restErr != nil { + log.Fatal(restErr) +} + +// Get the Card data of cards due today +notes, restErr := client.Cards.Get("prop:due=0") +if restErr != nil { + log.Fatal(restErr) +} + +``` + ### Sync local data to Anki Cloud ```go client := ankiconnect.NewClient() @@ -97,4 +135,4 @@ restErr := client.Sync.Trigger() if restErr != nil { log.Fatal(restErr) } -``` \ No newline at end of file +``` diff --git a/cards.go b/cards.go new file mode 100644 index 0000000..8d41b43 --- /dev/null +++ b/cards.go @@ -0,0 +1,69 @@ +package ankiconnect + +import "github.com/privatesquare/bkst-go-utils/utils/errors" + +const ( + ActionFindCards = "findCards" + ActionCardsInfo = "cardsInfo" +) + +type ( + // Notes manager describes the interface that can be used to perform operation on the notes in a deck. + CardsManager interface { + Search(query string) (*[]int64, *errors.RestErr) + Get(query string) (*[]ResultCardsInfo, *errors.RestErr) + } + + // notesManager implements NotesManager. + cardsManager struct { + Client *Client + } + + ParamsFindCards struct { + Query string `json:"query,omitempty"` + } + + ResultCardsInfo struct { + Answer string `json:"answer,omitempty"` + Question string `json:"question,omitempty"` + DeckName string `json:"deckName,omitempty"` + ModelName string `json:"modelName,omitempty"` + FieldOrder int64 `json:"fieldOrder,omitempty"` + Fields map[string]FieldData `json:"fields,omitempty"` + Css string `json:"css,omitempty"` + CardId int64 `json:"cardId,omitempty"` + Interval int64 `json:"interval,omitempty"` + Note int64 `json:"note,omitempty"` + Ord int64 `json:"ord,omitempty"` + Type int64 `json:"type,omitempty"` + Queue int64 `json:"queue,omitempty"` + Due int64 `json:"due,omitempty"` + Reps int64 `json:"reps,omitempty"` + Lapses int64 `json:"lapses,omitempty"` + Left int64 `json:"left,omitempty"` + Mod int64 `json:"mod,omitempty"` + } + + // ParamsCardsInfo represents the ankiconnect API params for getting card info. + ParamsCardsInfo struct { + Cards *[]int64 `json:"cards,omitempty"` + } +) + +func (cm *cardsManager) Search(query string) (*[]int64, *errors.RestErr) { + findParams := ParamsFindCards{ + Query: query, + } + return post[[]int64](cm.Client, ActionFindCards, &findParams) +} + +func (cm *cardsManager) Get(query string) (*[]ResultCardsInfo, *errors.RestErr) { + cardIds, restErr := cm.Search(query) + if restErr != nil { + return nil, restErr + } + infoParams := ParamsCardsInfo{ + Cards: cardIds, + } + return post[[]ResultCardsInfo](cm.Client, ActionCardsInfo, &infoParams) +} diff --git a/cards_test.go b/cards_test.go new file mode 100644 index 0000000..d3c7ec8 --- /dev/null +++ b/cards_test.go @@ -0,0 +1,63 @@ +package ankiconnect + +import ( + "net/http" + "testing" + + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" +) + +func TestCardsManager_Get(t *testing.T) { + findCardsPayload := []byte(`{ + "action": "findCards", + "version": 6, + "params": { + "query": "deck:current" + } + }`) + + cardsInfoPayload := []byte(`{ + "action": "cardsInfo", + "version": 6, + "params": { + "cards": [1498938915662, 1502098034048] + } + }`) + + t.Run("success", func(t *testing.T) { + defer httpmock.Reset() + + registerMultipleVerifiedPayloads(t, + [][2][]byte{ + // Get will do two api calls, first findCards to get the card id's + { + findCardsPayload, + loadTestResult(t, ActionFindCards), + }, + // Then cardsInfo to transform those into actual anki cards + { + cardsInfoPayload, + loadTestResult(t, ActionCardsInfo), + }, + }) + + payload := "deck:current" + notes, restErr := client.Cards.Get(payload) + assert.Nil(t, restErr) + assert.Equal(t, len(*notes), 2) + + }) + + t.Run("errorFailSearch", func(t *testing.T) { + defer httpmock.Reset() + + registerErrorResponse(t) + + _, restErr := client.Cards.Get("deck:current") + assert.NotNil(t, restErr) + assert.Equal(t, http.StatusBadRequest, restErr.StatusCode) + assert.Equal(t, "some error message", restErr.Message) + }) + +} diff --git a/client.go b/client.go index a41eafb..fd6f04b 100644 --- a/client.go +++ b/client.go @@ -24,9 +24,12 @@ type ( httpClient *resty.Client // supported interfaces - Decks DecksManager - Notes NotesManager - Sync SyncManager + Decks DecksManager + Notes NotesManager + Sync SyncManager + Cards CardsManager + Media MediaManager + Models ModelsManager } // RequestPayload represents the request payload for anki connect api. @@ -60,6 +63,9 @@ func NewClient() *Client { c.Decks = &decksManager{Client: c} c.Notes = ¬esManager{Client: c} c.Sync = &syncManager{Client: c} + c.Cards = &cardsManager{Client: c} + c.Media = &mediaManager{Client: c} + c.Models = &modelsManager{Client: c} return c } diff --git a/client_test.go b/client_test.go index d9b9fd9..18374d7 100644 --- a/client_test.go +++ b/client_test.go @@ -14,10 +14,6 @@ const ( ankiConnectTestVersion = 2 ) -var ( - client = NewClient() -) - func TestClient_NewClient(t *testing.T) { c := NewClient() assert.NotNil(t, c) @@ -61,24 +57,20 @@ func TestClient_SetSyncManager(t *testing.T) { func TestClient_Ping(t *testing.T) { t.Run("success", func(t *testing.T) { - c := NewClient() - httpmock.ActivateNonDefault(c.httpClient.GetClient()) - defer httpmock.DeactivateAndReset() + defer httpmock.Reset() responder, err := httpmock.NewJsonResponder(http.StatusOK, "") assert.NoError(t, err) httpmock.RegisterResponder(http.MethodGet, ankiConnectUrl, responder) - restErr := c.Ping() + restErr := client.Ping() assert.Nil(t, restErr) }) t.Run("failure", func(t *testing.T) { - c := NewClient() - httpmock.ActivateNonDefault(c.httpClient.GetClient()) - defer httpmock.DeactivateAndReset() + defer httpmock.Reset() - restErr := c.Ping() + restErr := client.Ping() assert.NotNil(t, restErr) assert.Equal(t, http.StatusServiceUnavailable, restErr.StatusCode) assert.Equal(t, ankiConnectPingErrMsg, restErr.Message) diff --git a/data/test/addNotePayload.json b/data/test/addNotePayload.json index 9fde48e..7c41ac9 100644 --- a/data/test/addNotePayload.json +++ b/data/test/addNotePayload.json @@ -47,4 +47,4 @@ }] } } -} \ No newline at end of file +} diff --git a/data/test/addNoteResult.json b/data/test/addNoteResult.json deleted file mode 100644 index 913dab3..0000000 --- a/data/test/addNoteResult.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "result": 1659294247478, - "error": null -} \ No newline at end of file diff --git a/data/test/cardsInfoResult.json b/data/test/cardsInfoResult.json new file mode 100644 index 0000000..bdd686e --- /dev/null +++ b/data/test/cardsInfoResult.json @@ -0,0 +1,50 @@ +{ + "result": [ + { + "answer": "back content", + "question": "front content", + "deckName": "Default", + "modelName": "Basic", + "fieldOrder": 1, + "fields": { + "Front": {"value": "front content", "order": 0}, + "Back": {"value": "back content", "order": 1} + }, + "css":"p {font-family:Arial;}", + "cardId": 1498938915662, + "interval": 16, + "note":1502298033753, + "ord": 1, + "type": 0, + "queue": 0, + "due": 1, + "reps": 1, + "lapses": 0, + "left": 6, + "mod": 1629454092 + }, + { + "answer": "back content", + "question": "front content", + "deckName": "Default", + "modelName": "Basic", + "fieldOrder": 0, + "fields": { + "Front": {"value": "front content", "order": 0}, + "Back": {"value": "back content", "order": 1} + }, + "css":"p {font-family:Arial;}", + "cardId": 1502098034048, + "interval": 23, + "note":1502298033753, + "ord": 1, + "type": 0, + "queue": 0, + "due": 1, + "reps": 1, + "lapses": 0, + "left": 6 + } + ], + "error": null +} diff --git a/data/test/createDeck.json b/data/test/createDeck.json deleted file mode 100644 index 36c0792..0000000 --- a/data/test/createDeck.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "result": 1659294179522, - "error": null -} \ No newline at end of file diff --git a/data/test/createModelExtraResult.json b/data/test/createModelExtraResult.json new file mode 100644 index 0000000..caf1e43 --- /dev/null +++ b/data/test/createModelExtraResult.json @@ -0,0 +1,58 @@ +{ + "result": { + "id": 1677087866636, + "name": "fish", + "type": 0, + "mod": 1677087866, + "usn": -1, + "sortf": 0, + "did": null, + "tmpls": [ + { + "name": "simple_fish_card", + "ord": 0, + "qfmt": "The fish is {{a}}", + "afmt": "The color is {{b}}", + "bqfmt": "", + "bafmt": "", + "did": null, + "bfont": "", + "bsize": 0 + } + ], + "flds": [ + { + "name": "a", + "ord": 0, + "sticky": false, + "rtl": false, + "font": "Arial", + "size": 20, + "description": "" + }, + { + "name": "b", + "ord": 1, + "sticky": false, + "rtl": false, + "font": "Arial", + "size": 20, + "description": "" + } + ], + "css": ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n", + "latexPre": "\\documentclass[12pt]{article}\n\\special{papersize=3in,5in}\n\\usepackage[utf8]{inputenc}\n\\usepackage{amssymb,amsmath}\n\\pagestyle{empty}\n\\setlength{\\parindent}{0in}\n\\begin{document}\n", + "latexPost": "\\end{document}", + "latexsvg": false, + "req": [ + [ + 0, + "any", + [ + 0 + ] + ] + ] + }, + "error": null +} diff --git a/data/test/createModelPayload.json b/data/test/createModelPayload.json new file mode 100644 index 0000000..53c0171 --- /dev/null +++ b/data/test/createModelPayload.json @@ -0,0 +1,17 @@ +{ + "action": "createModel", + "version": 6, + "params": { + "modelName": "newModelName", + "inOrderFields": ["Field1", "Field2", "Field3"], + "css": "Optional CSS with default to builtin css", + "isCloze": false, + "cardTemplates": [ + { + "Name": "My Card 1", + "Front": "Front html {{Field1}}", + "Back": "Back html {{Field2}}" + } + ] + } +} diff --git a/data/test/createModelResult.json b/data/test/createModelResult.json new file mode 100644 index 0000000..e9833d5 --- /dev/null +++ b/data/test/createModelResult.json @@ -0,0 +1,65 @@ +{ + "result":{ + "sortf":0, + "did":1, + "latexPre":"\\documentclass[12pt]{article}\n\\special{papersize=3in,5in}\n\\usepackage[utf8]{inputenc}\n\\usepackage{amssymb,amsmath}\n\\pagestyle{empty}\n\\setlength{\\parindent}{0in}\n\\begin{document}\n", + "latexPost":"\\end{document}", + "mod":1551462107, + "usn":-1, + "vers":[ + + ], + "type":0, + "css":".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n", + "name":"TestApiModel", + "flds":[ + { + "name":"Field1", + "ord":0, + "sticky":false, + "rtl":false, + "font":"Arial", + "size":20, + "media":[ + + ] + }, + { + "name":"Field2", + "ord":1, + "sticky":false, + "rtl":false, + "font":"Arial", + "size":20, + "media":[ + + ] + } + ], + "tmpls":[ + { + "name":"My Card 1", + "ord":0, + "qfmt":"", + "afmt":"This is the back of the card {{Field2}}", + "did":null, + "bqfmt":"", + "bafmt":"" + } + ], + "tags":[ + + ], + "id":"1551462107104", + "req":[ + [ + 0, + "none", + [ + + ] + ] + ] + }, + "error":null +} diff --git a/data/test/deckNames.json b/data/test/deckNames.json deleted file mode 100644 index 9f8c73d..0000000 --- a/data/test/deckNames.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "result": [ - "Default", - "Deck01", - "Deck02" - ], - "error": null -} \ No newline at end of file diff --git a/data/test/deleteDecks.json b/data/test/deleteDecks.json deleted file mode 100644 index 8ef91c1..0000000 --- a/data/test/deleteDecks.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "result": null, - "error": null -} \ No newline at end of file diff --git a/data/test/error.json b/data/test/error.json deleted file mode 100644 index 2bd4fc5..0000000 --- a/data/test/error.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "result": null, - "error": "some error message" -} \ No newline at end of file diff --git a/data/test/findCardsResult.json b/data/test/findCardsResult.json new file mode 100644 index 0000000..080c2d7 --- /dev/null +++ b/data/test/findCardsResult.json @@ -0,0 +1,4 @@ +{ + "result": [1498938915662, 1502098034048], + "error": null +} diff --git a/data/test/findNotesPayload.json b/data/test/findNotesPayload.json new file mode 100644 index 0000000..3d3c38f --- /dev/null +++ b/data/test/findNotesPayload.json @@ -0,0 +1,7 @@ +{ + "action": "findNotes", + "version": 6, + "params": { + "query": "deck:current" + } +} diff --git a/data/test/findNotesResult.json b/data/test/findNotesResult.json new file mode 100644 index 0000000..6b02988 --- /dev/null +++ b/data/test/findNotesResult.json @@ -0,0 +1,4 @@ +{ + "result": [1502298033753], + "error": null +} diff --git a/data/test/notesInfoResult.json b/data/test/notesInfoResult.json new file mode 100644 index 0000000..a2d0313 --- /dev/null +++ b/data/test/notesInfoResult.json @@ -0,0 +1,14 @@ +{ + "result": [ + { + "noteId":1502298033753, + "modelName": "Basic", + "tags":["tag","another_tag"], + "fields": { + "Front": {"value": "front content", "order": 0}, + "Back": {"value": "back content", "order": 1} + } + } + ], + "error": null +} diff --git a/data/test/updateNoteFieldsPayload.json b/data/test/updateNoteFieldsPayload.json new file mode 100644 index 0000000..25557f3 --- /dev/null +++ b/data/test/updateNoteFieldsPayload.json @@ -0,0 +1,21 @@ +{ + "action": "updateNoteFields", + "version": 6, + "params": { + "note": { + "id": 1514547547030, + "fields": { + "Front": "new front content", + "Back": "new back content" + }, + "audio": [{ + "url": "https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?kanji=猫&kana=ねこ", + "filename": "yomichan_ねこ_猫.mp3", + "skipHash": "7e2c2f954ef6051373ba916f000168dc", + "fields": [ + "Front" + ] + }] + } + } +} diff --git a/decks.go b/decks.go index d82c27a..bda9cba 100644 --- a/decks.go +++ b/decks.go @@ -39,8 +39,8 @@ type ( // GetAll retrieves all the decks from Anki. // The result is a slice of string with the names of the decks. // The method returns an error if: -// - the api request to ankiconnect fails. -// - the api returns a http error. +// - the api request to ankiconnect fails. +// - the api returns a http error. func (dm *decksManager) GetAll() (*[]string, *errors.RestErr) { result, restErr := post[[]string, ParamsDefault](dm.Client, ActionDeckNames, nil) if restErr != nil { @@ -51,8 +51,8 @@ func (dm *decksManager) GetAll() (*[]string, *errors.RestErr) { // Create creates a new deck in Anki. // The method returns an error if: -// - the api request to ankiconnect fails. -// - the api returns a http error. +// - the api request to ankiconnect fails. +// - the api returns a http error. func (dm *decksManager) Create(name string) *errors.RestErr { params := ParamsCreateDeck{ Deck: name, @@ -66,8 +66,8 @@ func (dm *decksManager) Create(name string) *errors.RestErr { // Delete deletes a deck from Anki // The method returns an error if: -// - the api request to ankiconnect fails. -// - the api returns a http error. +// - the api request to ankiconnect fails. +// - the api returns a http error. func (dm *decksManager) Delete(name string) *errors.RestErr { params := ParamsDeleteDecks{ Decks: &[]string{name}, diff --git a/decks_test.go b/decks_test.go index 2f1408d..570b47b 100644 --- a/decks_test.go +++ b/decks_test.go @@ -5,27 +5,26 @@ import ( "testing" "github.com/jarcoal/httpmock" - "github.com/privatesquare/bkst-go-utils/utils/fileutils" "github.com/stretchr/testify/assert" ) -const ( - testDataPath = "data/test/" - errorTestDataFileName = "error.json" - jsonExt = ".json" -) - func TestDecksManager_GetAll(t *testing.T) { + getAllRequest := []byte(`{ + "action": "deckNames", + "version": 6 +}`) + getAllResult := []byte(`{ + "result": [ + "Default", + "Deck01", + "Deck02" + ], + "error": null +}`) t.Run("success", func(t *testing.T) { - httpmock.ActivateNonDefault(client.httpClient.GetClient()) - defer httpmock.DeactivateAndReset() + defer httpmock.Reset() - result := new(Result[[]string]) - loadTestData(t, testDataPath+ActionDeckNames+jsonExt, result) - responder, err := httpmock.NewJsonResponder(http.StatusOK, result) - assert.NoError(t, err) - - httpmock.RegisterResponder(http.MethodPost, ankiConnectUrl, responder) + registerVerifiedPayload(t, getAllRequest, getAllResult) decks, restErr := client.Decks.GetAll() assert.NotNil(t, decks) @@ -34,15 +33,9 @@ func TestDecksManager_GetAll(t *testing.T) { }) t.Run("error", func(t *testing.T) { - httpmock.ActivateNonDefault(client.httpClient.GetClient()) - defer httpmock.DeactivateAndReset() - - result := new(Result[[]string]) - loadTestData(t, testDataPath+errorTestDataFileName, result) - responder, err := httpmock.NewJsonResponder(http.StatusOK, result) - assert.NoError(t, err) + defer httpmock.Reset() - httpmock.RegisterResponder(http.MethodPost, ankiConnectUrl, responder) + registerErrorResponse(t) decks, restErr := client.Decks.GetAll() assert.Nil(t, decks) @@ -52,8 +45,7 @@ func TestDecksManager_GetAll(t *testing.T) { }) t.Run("http request error", func(t *testing.T) { - httpmock.ActivateNonDefault(client.httpClient.GetClient()) - defer httpmock.DeactivateAndReset() + defer httpmock.Reset() decks, restErr := client.Decks.GetAll() assert.Nil(t, decks) @@ -64,31 +56,31 @@ func TestDecksManager_GetAll(t *testing.T) { } func TestDecksManager_Create(t *testing.T) { - t.Run("success", func(t *testing.T) { - httpmock.ActivateNonDefault(client.httpClient.GetClient()) - defer httpmock.DeactivateAndReset() + createRequest := []byte(`{ + "action": "createDeck", + "version": 6, + "params": { + "deck": "Japanese::Tokyo" + } +}`) + createResponse := []byte(`{ + "result": 1659294179522, + "error": null +}`) - result := new(Result[int64]) - loadTestData(t, testDataPath+ActionCreateDeck+jsonExt, result) - responder, err := httpmock.NewJsonResponder(http.StatusOK, result) - assert.NoError(t, err) + t.Run("success", func(t *testing.T) { + defer httpmock.Reset() - httpmock.RegisterResponder(http.MethodPost, ankiConnectUrl, responder) + registerVerifiedPayload(t, createRequest, createResponse) - restErr := client.Decks.Create("test") + restErr := client.Decks.Create("Japanese::Tokyo") assert.Nil(t, restErr) }) t.Run("error", func(t *testing.T) { - httpmock.ActivateNonDefault(client.httpClient.GetClient()) - defer httpmock.DeactivateAndReset() + defer httpmock.Reset() - result := new(Result[string]) - loadTestData(t, testDataPath+errorTestDataFileName, result) - responder, err := httpmock.NewJsonResponder(http.StatusOK, result) - assert.NoError(t, err) - - httpmock.RegisterResponder(http.MethodPost, ankiConnectUrl, responder) + registerErrorResponse(t) restErr := client.Decks.Create("test") assert.NotNil(t, restErr) @@ -98,31 +90,28 @@ func TestDecksManager_Create(t *testing.T) { } func TestDecksManagerDelete(t *testing.T) { - t.Run("success", func(t *testing.T) { - httpmock.ActivateNonDefault(client.httpClient.GetClient()) - defer httpmock.DeactivateAndReset() + deleteDeckRequest := []byte(`{ + "action": "deleteDecks", + "version": 6, + "params": { + "decks": ["test"], + "cardsToo": true + } +}`) - result := new(Result[string]) - loadTestData(t, testDataPath+ActionDeleteDecks+jsonExt, result) - responder, err := httpmock.NewJsonResponder(http.StatusOK, result) - assert.NoError(t, err) + t.Run("success", func(t *testing.T) { + defer httpmock.Reset() - httpmock.RegisterResponder(http.MethodPost, ankiConnectUrl, responder) + registerVerifiedPayload(t, deleteDeckRequest, genericSuccessJson) restErr := client.Decks.Delete("test") assert.Nil(t, restErr) }) t.Run("error", func(t *testing.T) { - httpmock.ActivateNonDefault(client.httpClient.GetClient()) - defer httpmock.DeactivateAndReset() + defer httpmock.Reset() - result := new(Result[string]) - loadTestData(t, testDataPath+errorTestDataFileName, result) - responder, err := httpmock.NewJsonResponder(http.StatusOK, result) - assert.NoError(t, err) - - httpmock.RegisterResponder(http.MethodPost, ankiConnectUrl, responder) + registerErrorResponse(t) restErr := client.Decks.Delete("test") assert.NotNil(t, restErr) @@ -130,8 +119,3 @@ func TestDecksManagerDelete(t *testing.T) { assert.Equal(t, "some error message", restErr.Message) }) } - -func loadTestData(t *testing.T, path string, out interface{}) { - err := fileutils.ReadJsonFile(path, &out) - assert.NoError(t, err) -} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..f8e4efa --- /dev/null +++ b/main_test.go @@ -0,0 +1,123 @@ +package ankiconnect + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "os" + "testing" + + "github.com/jarcoal/httpmock" + "github.com/privatesquare/bkst-go-utils/utils/fileutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + testDataPath = "data/test/" + jsonExt = ".json" +) + +var ( + client = NewClient() + errorResponse = Result[string]{} + genericErrorJson = []byte(`{ + "result": null, + "error": "some error message" +}`) + genericSuccessJson = []byte(`{ + "result": null, + "error": null +}`) +) + +// For each api call implemented, you test it as follows +// +// 1. Get the expected request and response from api description found at +// https://github.com/FooSoft/anki-connect +// +// 2. If the json blob is tiny, insert it in the test as a multiline string +// if its pretty large, it can be saved under +// data/test/{apiName}{Result/Payload}.json +// +// 3. You can then call some of the below helper functions to properly mock +// the http requests / responses +// +// 4) Do some assertions on the result of the api call +// +// The idea behind the this is that we want our tests to construct a go struct +// in the same way as the end user would using our defined structs. We then ensure +// that that go struct get correctly transformed into the json format expected by +// anki connect +func TestMain(m *testing.M) { + + httpmock.ActivateNonDefault(client.httpClient.GetClient()) + defer httpmock.DeactivateAndReset() + + os.Exit(m.Run()) +} + +// If you want your test to fail, call this before calling the api call +func registerErrorResponse(t *testing.T) { + json.Unmarshal(genericErrorJson, &errorResponse) + responder, err := httpmock.NewJsonResponder(http.StatusOK, errorResponse) + assert.NoError(t, err) + + httpmock.RegisterResponder(http.MethodPost, ankiConnectUrl, responder) +} + +// If you need to load a http request from a file, use this +func loadTestPayload(t *testing.T, ankiConnectAction string) []byte { + bytes, err := fileutils.ReadFile( + testDataPath + ankiConnectAction + "Payload.json") + assert.NoError(t, err) + return bytes +} + +// If you need to load a http response from a file, use this +func loadTestResult(t *testing.T, ankiConnectAction string) []byte { + bytes, err := fileutils.ReadFile( + testDataPath + ankiConnectAction + "Result.json") + assert.NoError(t, err) + return bytes +} + +// The loaded json strings then can be passed to this function, which will +// for each pair, verify that the client produces the correct json encoding +// and send the desired response +func registerMultipleVerifiedPayloads(t *testing.T, pairs [][2][]byte) { + currentIndex := 0 + httpmock.RegisterResponder(http.MethodPost, ankiConnectUrl, + func(req *http.Request) (*http.Response, error) { + + // If this assert fails the test is misconfigured + assert.Less(t, currentIndex, len(pairs), "Responder called too many times") + + currentPair := pairs[currentIndex] + payloadJson := currentPair[0] + responseJson := currentPair[1] + currentIndex += 1 + + bodyBytes, err := io.ReadAll(req.Body) + assert.NoError(t, err) + + require.JSONEq(t, string(payloadJson), string(bodyBytes)) + + // We cannot use NewJsonResponse since that serializes an interface + // Instead we just craft our own response with the right headers + resp := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBuffer(responseJson)), + Header: http.Header{"Content-Type": []string{"application/json"}}, + } + + return resp, nil + }, + ) +} + +// If you only are doing one Payload/Response, you can use this function +func registerVerifiedPayload(t *testing.T, payloadJson []byte, responseJson []byte) { + registerMultipleVerifiedPayloads(t, [][2][]byte{{payloadJson, responseJson}}) +} diff --git a/media.go b/media.go new file mode 100644 index 0000000..f3d7451 --- /dev/null +++ b/media.go @@ -0,0 +1,46 @@ +package ankiconnect + +import ( + "github.com/privatesquare/bkst-go-utils/utils/errors" +) + +const ( + ActionRetrieveMedia = "retrieveMediaFile" + // TODO + // storeMediaFile + // getMediaFileNames + // deleteMediaFile +) + +type ( + // Media describes the interface that can be used to perform operations stored media. + MediaManager interface { + // Returns the contents of the file encoded in base64 + RetrieveMediaFile(filename string) (*string, *errors.RestErr) + } + + ParamsRetrieveMediaFile struct { + Filename string `json:"filename,omitempty"` + } + + // mediaManager implements MediaManager. + mediaManager struct { + Client *Client + } +) + +// RetrieveMediaFile retrieve the contents of the named file from Anki. +// The result is a string with the base64-encoded contents. +// The method returns an error if: +// - the api request to ankiconnect fails. +// - the api returns a http error. +func (mm *mediaManager) RetrieveMediaFile(filename string) (*string, *errors.RestErr) { + params := ParamsRetrieveMediaFile{ + Filename: filename, + } + result, restErr := post[string](mm.Client, ActionRetrieveMedia, ¶ms) + if restErr != nil { + return nil, restErr + } + return result, nil +} diff --git a/media_test.go b/media_test.go new file mode 100644 index 0000000..b46e526 --- /dev/null +++ b/media_test.go @@ -0,0 +1,47 @@ +package ankiconnect + +import ( + "net/http" + "testing" + + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" +) + +func TestMediaManager_Retrieve(t *testing.T) { + retrieveMediaRequest := []byte(`{ + "action": "retrieveMediaFile", + "version": 6, + "params": { + "filename": "_hello.txt" + } + }`) + retrieveMediaResult := []byte(`{ + "result": "SGVsbG8sIHdvcmxkIQ==", + "error": null + }`) + + t.Run("success", func(t *testing.T) { + defer httpmock.Reset() + + registerVerifiedPayload(t, + retrieveMediaRequest, + retrieveMediaResult) + + data, restErr := client.Media.RetrieveMediaFile("_hello.txt") + assert.Equal(t, "SGVsbG8sIHdvcmxkIQ==", *data) + assert.Nil(t, restErr) + }) + + t.Run("error", func(t *testing.T) { + defer httpmock.Reset() + + registerErrorResponse(t) + + data, restErr := client.Media.RetrieveMediaFile("_hello.txt") + assert.Nil(t, data) + assert.NotNil(t, restErr) + assert.Equal(t, http.StatusBadRequest, restErr.StatusCode) + assert.Equal(t, "some error message", restErr.Message) + }) +} diff --git a/models.go b/models.go new file mode 100644 index 0000000..953248e --- /dev/null +++ b/models.go @@ -0,0 +1,126 @@ +package ankiconnect + +import ( + "encoding/json" + "github.com/privatesquare/bkst-go-utils/utils/errors" +) + +const ( + ActionModelNames = "modelNames" + ActionModelFieldNames = "modelFieldNames" + ActionCreateModel = "createModel" +) + +type ( + // Models Manager is used for creating various card and note types within Anki + ModelsManager interface { + Create(model Model) *errors.RestErr + GetAll() (*[]string, *errors.RestErr) + GetFields(model string) (*[]string, *errors.RestErr) + } + + // ParamsCreateModel is used for creating a new Note type to add a new card to an + // existing Note type will require the implementation of updateModelTemplates + Model struct { + ModelName string `json:"modelName,omitempty"` + InOrderFields []string `json:"inOrderFields,omitempty"` + Css string `json:"css,omitempty"` + // Will default to false + IsCloze bool `json:"isCloze"` + CardTemplates []CardTemplate `json:"cardTemplates,omitempty"` + } + + // CardTemplate contains the actual fields that will determine the + // front and back of the anki card + CardTemplate struct { + Name string `json:"Name,omitempty"` + Front string `json:"Front,omitempty"` + Back string `json:"Back,omitempty"` + } + + // modelsManager implements ModelsManager. + modelsManager struct { + Client *Client + } + + // ParamsModelNames represents the ankiconnect API params required for + // querying the Model Names avaliable + ParamsModelNames struct { + ModelName string `json:"modelName"` + } + + // ResultCreateModel represents the ankiconnect API result from + // creating a new model (Note type) + // + // The example given has some empty arrays. Since we dont know the data + // types, and its not currently important we leave them as []interface{} + // We do not technically need this, and could replace it with a [interface{}] + // in Create, but it may be used later + ResultCreateModel struct { + Sortf int64 `json:"sortf"` + Did int64 `json:"did"` + LatexPre string `json:"latexPre"` + LatexPost string `json:"latexPost"` + Mod int64 `json:"mod"` + Usn int64 `json:"usn"` + Vers []interface{} `json:"vers"` + Type int64 `json:"type"` + Css string `json:"css"` + Name string `json:"name"` + Flds []struct { + Name string `json:"name"` + Ord int64 `json:"ord"` + Sticky bool `json:"sticky"` + Rtl bool `json:"rtl"` + Font string `json:"font"` + Size int64 `json:"size"` + Media []interface{} `json:"media"` + } `json:"flds"` + Tmpls []struct { + Name string `json:"name"` + Ord int64 `json:"ord"` + Qfmt string `json:"qfmt"` + Afmt string `json:"afmt"` + Did interface{} `json:"did"` + Bqfmt string `json:"bqfmt"` + Bafmt string `json:"bafmt"` + } `json:"tmpls"` + Tags []interface{} `json:"tags"` + // The api description describes the id as being a "number" (eg "23443") + // Where as in practice anki seems to return an actual int (eg 23443). + // json.Number handles both instances + Id json.Number `json:"id"` + Req [][]interface{} `json:"req"` + } +) + +// The method returns an error if: +// - the api request to ankiconnect fails. +// - the api returns a http error. +func (mm *modelsManager) Create(model Model) *errors.RestErr { + // This thing returns a large complicated struct back, ignore it for now + _, restErr := post[ResultCreateModel](mm.Client, ActionCreateModel, &model) + if restErr != nil { + return restErr + } + return nil +} + +func (mm *modelsManager) GetAll() (*[]string, *errors.RestErr) { + modelNames, restErr := post[[]string, ParamsDefault](mm.Client, ActionModelNames, nil) + if restErr != nil { + return nil, restErr + } + return modelNames, nil +} +func (mm *modelsManager) GetFields(model string) (*[]string, *errors.RestErr) { + modelName := ParamsModelNames{ + ModelName: model, + } + modelFields, restErr := post[[]string](mm.Client, ActionModelFieldNames, &modelName) + if restErr != nil { + return nil, restErr + } + return modelFields, nil + +} diff --git a/models_test.go b/models_test.go new file mode 100644 index 0000000..160d539 --- /dev/null +++ b/models_test.go @@ -0,0 +1,136 @@ +package ankiconnect + +import ( + "net/http" + "testing" + + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" +) + +func TestModelsManager_Create(t *testing.T) { + newModel := Model{ + ModelName: "newModelName", + InOrderFields: []string{"Field1", "Field2", "Field3"}, + Css: "Optional CSS with default to builtin css", + IsCloze: false, + CardTemplates: []CardTemplate{ + { + Name: "My Card 1", + Front: "Front html {{Field1}}", + Back: "Back html {{Field2}}", + }, + }, + } + + t.Run("success", func(t *testing.T) { + defer httpmock.Reset() + + registerVerifiedPayload(t, + loadTestPayload(t, ActionCreateModel), + loadTestResult(t, ActionCreateModel)) + + restErr := client.Models.Create(newModel) + assert.Nil(t, restErr) + }) + + t.Run("realData", func(t *testing.T) { + // When running this against a real instance of anki, + // The returned result was a little different. So test to + // make sure we can handle both + defer httpmock.Reset() + + registerVerifiedPayload(t, + loadTestPayload(t, ActionCreateModel), + loadTestResult(t, ActionCreateModel+"Extra")) + + restErr := client.Models.Create(newModel) + assert.Nil(t, restErr) + }) + + t.Run("error", func(t *testing.T) { + defer httpmock.Reset() + + registerErrorResponse(t) + + restErr := client.Models.Create(newModel) + assert.NotNil(t, restErr) + assert.Equal(t, http.StatusBadRequest, restErr.StatusCode) + assert.Equal(t, "some error message", restErr.Message) + }) +} + +func TestModelsManager_GetAll(t *testing.T) { + modelNamesPayload := []byte(`{ + "action": "modelNames", + "version": 6 + }`) + modelNamesResult := []byte(`{ + "result": ["Basic", "Basic (and reversed card)"], + "error": null + }`) + + t.Run("success", func(t *testing.T) { + defer httpmock.Reset() + + registerVerifiedPayload(t, + modelNamesPayload, + modelNamesResult) + + names, restErr := client.Models.GetAll() + assert.Nil(t, restErr) + assert.Len(t, *names, 2) + }) + + t.Run("error", func(t *testing.T) { + defer httpmock.Reset() + + registerErrorResponse(t) + + names, restErr := client.Models.GetAll() + assert.Nil(t, names) + assert.NotNil(t, restErr) + assert.Equal(t, http.StatusBadRequest, restErr.StatusCode) + assert.Equal(t, "some error message", restErr.Message) + }) + +} + +func TestModelsManager_GetFields(t *testing.T) { + modelFieldsPayload := []byte(`{ + "action": "modelFieldNames", + "version": 6, + "params": { + "modelName": "Basic" + } + }`) + modelFieldsResult := []byte(`{ + "result": ["Front", "Back"], + "error": null + }`) + + t.Run("success", func(t *testing.T) { + defer httpmock.Reset() + + registerVerifiedPayload(t, + modelFieldsPayload, + modelFieldsResult) + + fields, restErr := client.Models.GetFields("Basic") + assert.Nil(t, restErr) + assert.Len(t, *fields, 2) + }) + + t.Run("error", func(t *testing.T) { + defer httpmock.Reset() + + registerErrorResponse(t) + + fields, restErr := client.Models.GetFields("Basic") + assert.Nil(t, fields) + assert.NotNil(t, restErr) + assert.Equal(t, http.StatusBadRequest, restErr.StatusCode) + assert.Equal(t, "some error message", restErr.Message) + }) + +} diff --git a/notes.go b/notes.go index 0a070f1..683f06e 100644 --- a/notes.go +++ b/notes.go @@ -3,17 +3,21 @@ package ankiconnect import "github.com/privatesquare/bkst-go-utils/utils/errors" const ( - ActionFindNotes = "findNotes" - ActionNotesInfo = "notesInfo" - ActionAddNote = "addNote" - ActionAddNotes = "addNotes" - ActionDeleteNotes = "deleteNotes" + ActionFindNotes = "findNotes" + ActionNotesInfo = "notesInfo" + ActionAddNote = "addNote" + ActionAddNotes = "addNotes" + ActionDeleteNotes = "deleteNotes" + ActionUpdateNoteFields = "updateNoteFields" ) type ( // Notes manager describes the interface that can be used to perform operation on the notes in a deck. NotesManager interface { Add(note Note) *errors.RestErr + Search(query string) (*[]int64, *errors.RestErr) + Get(query string) (*[]ResultNotesInfo, *errors.RestErr) + Update(note UpdateNote) *errors.RestErr } // notesManager implements NotesManager. @@ -26,6 +30,29 @@ type ( Note *Note `json:"note,omitempty"` } + // ParamsCreateNote represents the ankiconnect API params for updating a note. + ParamsUpdateNote struct { + Note *UpdateNote `json:"note,omitempty"` + } + + // ParamsGetNotes represents the ankiconnect API params for querying notes. + ParamsFindNotes struct { + Query string `json:"query,omitempty"` + } + + // ResultFindNotes represents the return value of querying notes + ResultNotesInfo struct { + NoteId int64 `json:"noteId,omitempty"` + ModelName string `json:"modelName,omitempty"` + Fields map[string]FieldData `json:"fields,omitempty"` + Tags []string `json:"tags,omitempty"` + } + + // ParamsNotesInfo represents the ankiconnect API params for getting note info. + ParamsNotesInfo struct { + Notes *[]int64 `json:"notes,omitempty"` + } + // Note represents a Anki Note. Note struct { DeckName string `json:"deckName,omitempty"` @@ -38,29 +65,45 @@ type ( Picture []Picture `json:"picture,omitempty"` } + UpdateNote struct { + Id int64 `json:"id,omitempty"` + Fields Fields `json:"fields,omitempty"` + Audio []Audio `json:"audio,omitempty"` + Video []Video `json:"video,omitempty"` + Picture []Picture `json:"picture,omitempty"` + } + // Fields represents the main fields for a Anki Note - Fields struct { - Front string `json:"Front,omitempty"` - Back string `json:"Back,omitempty"` + Fields map[string]string + + // FieldData represents the format of a field returned by ankiconnect + FieldData struct { + Value string `json:"value,omitempty"` + Order int64 `json:"order,omitempty"` } // Options represents note options. Options struct { - AllowDuplicate bool `json:"allowDuplicate,omitempty"` + // Will default to false + AllowDuplicate bool `json:"allowDuplicate"` DuplicateScope string `json:"duplicateScope,omitempty"` DuplicateScopeOptions *DuplicateScopeOptions `json:"duplicateScopeOptions,omitempty"` } // DuplicateScopeOptions represents the options that control the duplication of a Anki Note. DuplicateScopeOptions struct { - DeckName string `json:"deckName,omitempty"` - CheckChildren bool `json:"checkChildren,omitempty"` - CheckAllModels bool `json:"checkAllModels,omitempty"` + DeckName string `json:"deckName,omitempty"` + // Will default to false + CheckChildren bool `json:"checkChildren"` + // Will default to false + CheckAllModels bool `json:"checkAllModels"` } // Audio can be used to add a audio file to a Anki Note. Audio struct { URL string `json:"url,omitempty"` + Data string `json:"data,omitempty"` + Path string `json:"path,omitempty"` Filename string `json:"filename,omitempty"` SkipHash string `json:"skipHash,omitempty"` Fields []string `json:"fields,omitempty"` @@ -69,6 +112,8 @@ type ( // Video can be used to add a video file to a Anki Note. Video struct { URL string `json:"url,omitempty"` + Data string `json:"data,omitempty"` + Path string `json:"path,omitempty"` Filename string `json:"filename,omitempty"` SkipHash string `json:"skipHash,omitempty"` Fields []string `json:"fields,omitempty"` @@ -77,6 +122,8 @@ type ( // Picture can be used to add a picture to a Anki Note. Picture struct { URL string `json:"url,omitempty"` + Data string `json:"data,omitempty"` + Path string `json:"path,omitempty"` Filename string `json:"filename,omitempty"` SkipHash string `json:"skipHash,omitempty"` Fields []string `json:"fields,omitempty"` @@ -85,8 +132,8 @@ type ( // Add adds a new note in Anki. // The method returns an error if: -// - the api request to ankiconnect fails. -// - the api returns a http error. +// - the api request to ankiconnect fails. +// - the api returns a http error. func (nm *notesManager) Add(note Note) *errors.RestErr { params := ParamsCreateNote{ Note: ¬e, @@ -97,3 +144,31 @@ func (nm *notesManager) Add(note Note) *errors.RestErr { } return nil } + +func (nm *notesManager) Search(query string) (*[]int64, *errors.RestErr) { + findParams := ParamsFindNotes{ + Query: query, + } + return post[[]int64](nm.Client, ActionFindNotes, &findParams) +} + +func (nm *notesManager) Get(query string) (*[]ResultNotesInfo, *errors.RestErr) { + noteIds, restErr := nm.Search(query) + if restErr != nil { + return nil, restErr + } + infoParams := ParamsNotesInfo{ + Notes: noteIds, + } + return post[[]ResultNotesInfo](nm.Client, ActionNotesInfo, &infoParams) +} + +func (nm *notesManager) Update(note UpdateNote) *errors.RestErr { + params := ParamsUpdateNote{ + Note: ¬e, + } + // The return of this should always be 'null' int64 may not be the best + // type here + _, restErr := post[int64](nm.Client, ActionUpdateNoteFields, ¶ms) + return restErr +} diff --git a/notes_test.go b/notes_test.go index 6ae46cf..1e8be79 100644 --- a/notes_test.go +++ b/notes_test.go @@ -9,39 +9,160 @@ import ( ) func TestNotesManager_Add(t *testing.T) { - t.Run("success", func(t *testing.T) { - httpmock.ActivateNonDefault(client.httpClient.GetClient()) - defer httpmock.DeactivateAndReset() + createNoteStruct := Note{ + DeckName: "test", + ModelName: "Basic-a39a1", + Fields: Fields{ + "Front": "front content", + "Back": "back content", + }, + Options: &Options{ + AllowDuplicate: false, + DuplicateScope: "deck", + DuplicateScopeOptions: &DuplicateScopeOptions{ + DeckName: "test", + CheckChildren: false, + CheckAllModels: false, + }, + }, + Tags: []string{ + "yomichan", + }, + Audio: []Audio{ + { + URL: "https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?kanji=猫&kana=ねこ", + Filename: "yomichan_ねこ_猫.mp3", + SkipHash: "7e2c2f954ef6051373ba916f000168dc", + Fields: []string{ + "Front", + }, + }, + }, + Video: []Video{ + { + URL: "https://cdn.videvo.net/videvo_files/video/free/2015-06/small_watermarked/Contador_Glam_preview.mp4", + Filename: "countdown.mp4", + SkipHash: "4117e8aab0d37534d9c8eac362388bbe", + Fields: []string{ + "Back", + }, + }, + }, + Picture: []Picture{ + { + URL: "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c7/A_black_cat_named_Tilly.jpg/220px-A_black_cat_named_Tilly.jpg", + Filename: "black_cat.jpg", + SkipHash: "8d6e4646dfae812bf39651b59d7429ce", + Fields: []string{ + "Back", + }, + }, + }, + } + + addNoteResult := []byte(`{ + "result": 1659294247478, + "error": null + }`) - result := new(Result[int64]) - loadTestData(t, testDataPath+ActionAddNote+"Result"+jsonExt, result) - responder, err := httpmock.NewJsonResponder(http.StatusOK, result) - assert.NoError(t, err) + t.Run("success", func(t *testing.T) { + defer httpmock.Reset() - httpmock.RegisterResponder(http.MethodPost, ankiConnectUrl, responder) + registerVerifiedPayload(t, + loadTestPayload(t, ActionAddNote), + addNoteResult) - note := new(Note) - loadTestData(t, testDataPath+ActionAddNote+"Payload"+jsonExt, result) - restErr := client.Notes.Add(*note) + note := createNoteStruct + restErr := client.Notes.Add(note) assert.Nil(t, restErr) }) t.Run("error", func(t *testing.T) { - httpmock.ActivateNonDefault(client.httpClient.GetClient()) - defer httpmock.DeactivateAndReset() + defer httpmock.Reset() + + registerErrorResponse(t) + + note := createNoteStruct + restErr := client.Notes.Add(note) + assert.NotNil(t, restErr) + assert.Equal(t, http.StatusBadRequest, restErr.StatusCode) + assert.Equal(t, "some error message", restErr.Message) + }) +} - result := new(Result[string]) - loadTestData(t, testDataPath+errorTestDataFileName, result) - responder, err := httpmock.NewJsonResponder(http.StatusOK, result) - assert.NoError(t, err) +func TestNotesManager_Get(t *testing.T) { + notesInfoPayload := []byte(`{ + "action": "notesInfo", + "version": 6, + "params": { + "notes": [1502298033753] + } + }`) + t.Run("success", func(t *testing.T) { + defer httpmock.Reset() - httpmock.RegisterResponder(http.MethodPost, ankiConnectUrl, responder) + registerMultipleVerifiedPayloads(t, + [][2][]byte{ + // Get will do two api calls, first findNotes to get the card id's + { + loadTestPayload(t, ActionFindNotes), + loadTestResult(t, ActionFindNotes), + }, + // Then notesInfo to transform those into actual anki cards + { + notesInfoPayload, + loadTestResult(t, ActionNotesInfo), + }, + }) - note := new(Note) - loadTestData(t, testDataPath+ActionAddNote+"Payload"+jsonExt, result) - restErr := client.Notes.Add(*note) + payload := "deck:current" + notes, restErr := client.Notes.Get(payload) + assert.Nil(t, restErr) + assert.Equal(t, len(*notes), 1) + + }) + + t.Run("errorFailSearch", func(t *testing.T) { + defer httpmock.Reset() + + registerErrorResponse(t) + + _, restErr := client.Notes.Get("deck:current") assert.NotNil(t, restErr) assert.Equal(t, http.StatusBadRequest, restErr.StatusCode) assert.Equal(t, "some error message", restErr.Message) }) } + +func TestNotesManager_Update(t *testing.T) { + updateNoteStruct := UpdateNote{ + Id: 1514547547030, + Fields: Fields{ + "Front": "new front content", + "Back": "new back content", + }, + Audio: []Audio{ + { + URL: "https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?kanji=猫&kana=ねこ", + Filename: "yomichan_ねこ_猫.mp3", + SkipHash: "7e2c2f954ef6051373ba916f000168dc", + Fields: []string{ + "Front", + }, + }, + }, + } + + t.Run("success", func(t *testing.T) { + defer httpmock.Reset() + + registerVerifiedPayload(t, + loadTestPayload(t, ActionUpdateNoteFields), + genericSuccessJson) + + restErr := client.Notes.Update(updateNoteStruct) + assert.Nil(t, restErr) + + }) + +} diff --git a/sync.go b/sync.go index 794d074..c193f60 100644 --- a/sync.go +++ b/sync.go @@ -13,15 +13,15 @@ type ( } // syncManager implements SyncManager - syncManager struct{ + syncManager struct { Client *Client } ) // Trigger syncs local Anki data to Anki web. // The method returns an error if: -// - the api request to ankiconnect fails. -// - the api returns a http error. +// - the api request to ankiconnect fails. +// - the api returns a http error. func (sm *syncManager) Trigger() *errors.RestErr { _, restErr := post[string, ParamsDefault](sm.Client, ActionSync, nil) if restErr != nil { diff --git a/sync_test.go b/sync_test.go index 03f1175..148a795 100644 --- a/sync_test.go +++ b/sync_test.go @@ -9,30 +9,29 @@ import ( ) func TestSyncManager_Trigger(t *testing.T) { - t.Run("success", func(t *testing.T) { - httpmock.ActivateNonDefault(client.httpClient.GetClient()) - defer httpmock.DeactivateAndReset() - result := new(Result[string]) - responder, err := httpmock.NewJsonResponder(http.StatusOK, result) - assert.NoError(t, err) + syncRequest := []byte(`{ + "action": "sync", + "version": 6 +}`) + syncResult := []byte(`{ + "result": null, + "error": null +}`) + + t.Run("success", func(t *testing.T) { + defer httpmock.Reset() - httpmock.RegisterResponder(http.MethodPost, ankiConnectUrl, responder) + registerVerifiedPayload(t, syncRequest, syncResult) restErr := client.Sync.Trigger() assert.Nil(t, restErr) }) t.Run("error", func(t *testing.T) { - httpmock.ActivateNonDefault(client.httpClient.GetClient()) - defer httpmock.DeactivateAndReset() - - result := new(Result[string]) - loadTestData(t, testDataPath+errorTestDataFileName, result) - responder, err := httpmock.NewJsonResponder(http.StatusOK, result) - assert.NoError(t, err) + defer httpmock.Reset() - httpmock.RegisterResponder(http.MethodPost, ankiConnectUrl, responder) + registerErrorResponse(t) restErr := client.Sync.Trigger() assert.NotNil(t, restErr)