diff --git a/block.go b/block.go index de0b4bc..85216a2 100644 --- a/block.go +++ b/block.go @@ -10,10 +10,11 @@ import ( type Block struct { Object string `json:"object"` ID string `json:"id,omitempty"` - Type BlockType `json:"type"` + Type BlockType `json:"type,omitempty"` CreatedTime *time.Time `json:"created_time,omitempty"` LastEditedTime *time.Time `json:"last_edited_time,omitempty"` HasChildren bool `json:"has_children,omitempty"` + Archived *bool `json:"archived,omitempty"` Paragraph *RichTextBlock `json:"paragraph,omitempty"` Heading1 *Heading `json:"heading_1,omitempty"` diff --git a/client.go b/client.go index c313b09..7322776 100644 --- a/client.go +++ b/client.go @@ -363,6 +363,39 @@ func (c *Client) FindBlockByID(ctx context.Context, blockID string) (block Block return block, nil } +// UpdateBlock updates a block. +// See: https://developers.notion.com/reference/update-a-block +func (c *Client) UpdateBlock(ctx context.Context, blockID string, block Block) (updatedBlock Block, err error) { + body := &bytes.Buffer{} + + err = json.NewEncoder(body).Encode(block) + if err != nil { + return Block{}, fmt.Errorf("notion: failed to encode body params to JSON: %w", err) + } + + req, err := c.newRequest(ctx, http.MethodPatch, "/blocks/"+blockID, body) + if err != nil { + return Block{}, fmt.Errorf("notion: invalid request: %w", err) + } + + res, err := c.httpClient.Do(req) + if err != nil { + return Block{}, fmt.Errorf("notion: failed to make HTTP request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return Block{}, fmt.Errorf("notion: failed to update block: %w", parseErrorResponse(res)) + } + + err = json.NewDecoder(res.Body).Decode(&updatedBlock) + if err != nil { + return Block{}, fmt.Errorf("notion: failed to parse HTTP response: %w", err) + } + + return updatedBlock, nil +} + // FindUserByID fetches a user by ID. // See: https://developers.notion.com/reference/get-user func (c *Client) FindUserByID(ctx context.Context, id string) (user User, err error) { diff --git a/client_test.go b/client_test.go index eb0c7c9..6d58727 100644 --- a/client_test.go +++ b/client_test.go @@ -3101,6 +3101,7 @@ func TestFindBlockByID(t *testing.T) { LastEditedTime: mustParseTimePointer(time.RFC3339, "2021-10-02T06:31:00Z"), HasChildren: true, ChildPage: ¬ion.ChildPage{Title: "test title"}, + Archived: notion.BoolPtr(false), }, expError: nil, }, @@ -3155,3 +3156,196 @@ func TestFindBlockByID(t *testing.T) { }) } } + +func TestUpdateBlock(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + block notion.Block + respBody func(r *http.Request) io.Reader + respStatusCode int + expPostBody map[string]interface{} + expResponse notion.Block + expError error + }{ + { + name: "successful response", + block: notion.Block{ + Paragraph: ¬ion.RichTextBlock{ + Text: []notion.RichText{ + { + Text: ¬ion.Text{ + Content: "Foobar", + }, + }, + }, + }, + }, + respBody: func(_ *http.Request) io.Reader { + return strings.NewReader( + `{ + "object": "block", + "id": "048e165e-352d-4119-8128-e46c3527d95c", + "created_time": "2021-10-02T06:09:00.000Z", + "last_edited_time": "2021-10-02T06:31:00.000Z", + "has_children": true, + "archived": false, + "type": "paragraph", + "paragraph": { + "text": [ + { + "type": "text", + "text": { + "content": "Foobar", + "link": null + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "plain_text": "Foobar", + "href": null + } + ] + } + }`, + ) + }, + respStatusCode: http.StatusOK, + expPostBody: map[string]interface{}{ + "object": "block", + "paragraph": map[string]interface{}{ + "text": []interface{}{ + map[string]interface{}{ + "text": map[string]interface{}{ + "content": "Foobar", + }, + }, + }, + }, + }, + expResponse: notion.Block{ + Object: "block", + ID: "048e165e-352d-4119-8128-e46c3527d95c", + Type: notion.BlockTypeParagraph, + CreatedTime: mustParseTimePointer(time.RFC3339, "2021-10-02T06:09:00Z"), + LastEditedTime: mustParseTimePointer(time.RFC3339, "2021-10-02T06:31:00Z"), + HasChildren: true, + Paragraph: ¬ion.RichTextBlock{ + Text: []notion.RichText{ + { + Type: notion.RichTextTypeText, + Text: ¬ion.Text{ + Content: "Foobar", + }, + PlainText: "Foobar", + Annotations: ¬ion.Annotations{ + Color: notion.ColorDefault, + }, + }, + }, + }, + Archived: notion.BoolPtr(false), + }, + expError: nil, + }, + { + name: "error response", + block: notion.Block{ + Paragraph: ¬ion.RichTextBlock{ + Text: []notion.RichText{ + { + Text: ¬ion.Text{ + Content: "Foobar", + }, + }, + }, + }, + }, + respBody: func(_ *http.Request) io.Reader { + return strings.NewReader( + `{ + "object": "error", + "status": 400, + "code": "validation_error", + "message": "foobar" + }`, + ) + }, + respStatusCode: http.StatusBadRequest, + expPostBody: map[string]interface{}{ + "object": "block", + "paragraph": map[string]interface{}{ + "text": []interface{}{ + map[string]interface{}{ + "text": map[string]interface{}{ + "content": "Foobar", + }, + }, + }, + }, + }, + expResponse: notion.Block{}, + expError: errors.New("notion: failed to update block: foobar (code: validation_error, status: 400)"), + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + httpClient := &http.Client{ + Transport: &mockRoundtripper{fn: func(r *http.Request) (*http.Response, error) { + postBody := make(map[string]interface{}) + + err := json.NewDecoder(r.Body).Decode(&postBody) + if err != nil && err != io.EOF { + t.Fatal(err) + } + + if len(tt.expPostBody) == 0 && len(postBody) != 0 { + t.Errorf("unexpected post body: %#v", postBody) + } + + if len(tt.expPostBody) != 0 && len(postBody) == 0 { + t.Errorf("post body not equal (expected %+v, got: nil)", tt.expPostBody) + } + + if len(tt.expPostBody) != 0 && len(postBody) != 0 { + if diff := cmp.Diff(tt.expPostBody, postBody); diff != "" { + t.Errorf("post body not equal (-exp, +got):\n%v", diff) + } + } + + return &http.Response{ + StatusCode: tt.respStatusCode, + Status: http.StatusText(tt.respStatusCode), + Body: ioutil.NopCloser(tt.respBody(r)), + }, nil + }}, + } + client := notion.NewClient("secret-api-key", notion.WithHTTPClient(httpClient)) + updatedBlock, err := client.UpdateBlock(context.Background(), "00000000-0000-0000-0000-000000000000", tt.block) + + if tt.expError == nil && err != nil { + t.Fatalf("unexpected error: %v", err) + } + if tt.expError != nil && err == nil { + t.Fatalf("error not equal (expected: %v, got: nil)", tt.expError) + } + if tt.expError != nil && err != nil && tt.expError.Error() != err.Error() { + t.Fatalf("error not equal (expected: %v, got: %v)", tt.expError, err) + } + + if diff := cmp.Diff(tt.expResponse, updatedBlock); diff != "" { + t.Fatalf("response not equal (-exp, +got):\n%v", diff) + } + }) + } +}