From 78b14fa941888006aa5adfb1249f9b2e57f4eb47 Mon Sep 17 00:00:00 2001 From: Florian Kinder Date: Wed, 18 Sep 2024 17:09:14 +0900 Subject: [PATCH] Added confluence template api Took 2 hours 39 minutes --- confluence/api_client_impl.go | 2 + confluence/internal/template_impl.go | 110 +++++++ confluence/internal/template_impl_test.go | 380 ++++++++++++++++++++++ pkg/infra/models/confluence_template.go | 109 +++++++ service/confluence/template.go | 30 ++ 5 files changed, 631 insertions(+) create mode 100644 confluence/internal/template_impl.go create mode 100644 confluence/internal/template_impl_test.go create mode 100644 pkg/infra/models/confluence_template.go create mode 100644 service/confluence/template.go diff --git a/confluence/api_client_impl.go b/confluence/api_client_impl.go index 6c4f5104..dcff2ab3 100644 --- a/confluence/api_client_impl.go +++ b/confluence/api_client_impl.go @@ -60,6 +60,7 @@ func New(httpClient common.HTTPClient, site string) (*Client, error) { client.Search = internal.NewSearchService(client) client.LongTask = internal.NewTaskService(client) client.Analytics = internal.NewAnalyticsService(client) + client.Template = internal.NewTemplateService(client) return client, nil } @@ -74,6 +75,7 @@ type Client struct { Search *internal.SearchService LongTask *internal.TaskService Analytics *internal.AnalyticsService + Template *internal.TemplateService } func (c *Client) NewRequest(ctx context.Context, method, urlStr, contentType string, body interface{}) (*http.Request, error) { diff --git a/confluence/internal/template_impl.go b/confluence/internal/template_impl.go new file mode 100644 index 00000000..f3ab5254 --- /dev/null +++ b/confluence/internal/template_impl.go @@ -0,0 +1,110 @@ +package internal + +import ( + "context" + "net/http" + + "github.com/ctreminiom/go-atlassian/pkg/infra/models" + "github.com/ctreminiom/go-atlassian/service" + "github.com/ctreminiom/go-atlassian/service/confluence" +) + +// NewTemplateService creates a new instance of TemplateService. +// It takes a service.Connector as inputs and returns a pointer to TemplateService. +func NewTemplateService(client service.Connector) *TemplateService { + return &TemplateService{ + internalClient: &internalTemplateImpl{c: client}, + } +} + +// TemplateService provides methods to interact with template operations in Confluence. +type TemplateService struct { + // internalClient is the connector interface for content operations. + internalClient confluence.TemplateConnector +} + +// Create creates a new template. +// +// POST /wiki/rest/api/template +// +// https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-template/#api-wiki-rest-api-template-post +func (t *TemplateService) Create(ctx context.Context, payload *models.CreateTemplateScheme) (*models.ContentTemplateScheme, *models.ResponseScheme, error) { + return t.internalClient.Create(ctx, payload) +} + +// Update updates a template. +// +// PUT /wiki/rest/api/template +// +// https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-template/#api-wiki-rest-api-template-put +func (t *TemplateService) Update(ctx context.Context, payload *models.UpdateTemplateScheme) (*models.ContentTemplateScheme, *models.ResponseScheme, error) { + return t.internalClient.Update(ctx, payload) +} + +// Get content template by ID. +// +// GET /wiki/rest/api/template/{id} +// +// https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-template/#api-wiki-rest-api-template-contenttemplateid-get +func (t *TemplateService) Get(ctx context.Context, templateID string) (*models.ContentTemplateScheme, *models.ResponseScheme, error) { + return t.internalClient.Get(ctx, templateID) +} + +// internalTemplateImpl is the internal implementation of TemplateService. +type internalTemplateImpl struct { + c service.Connector +} + +// Create implements TemplateService.Create. +func (i *internalTemplateImpl) Create(ctx context.Context, payload *models.CreateTemplateScheme) (*models.ContentTemplateScheme, *models.ResponseScheme, error) { + endpoint := "/wiki/rest/api/template" + + request, err := i.c.NewRequest(ctx, http.MethodPost, endpoint, "", payload) + if err != nil { + return nil, nil, err + } + + result := new(models.ContentTemplateScheme) + response, err := i.c.Call(request, result) + if err != nil { + return nil, response, err + } + + return result, response, nil +} + +// Update implements TemplateService.Update. +func (i *internalTemplateImpl) Update(ctx context.Context, payload *models.UpdateTemplateScheme) (*models.ContentTemplateScheme, *models.ResponseScheme, error) { + endpoint := "/wiki/rest/api/template" + + request, err := i.c.NewRequest(ctx, http.MethodPut, endpoint, "", payload) + if err != nil { + return nil, nil, err + } + + result := new(models.ContentTemplateScheme) + response, err := i.c.Call(request, result) + if err != nil { + return nil, response, err + } + + return result, response, nil +} + +// Get implements TemplateService.Get. +func (i *internalTemplateImpl) Get(ctx context.Context, templateID string) (*models.ContentTemplateScheme, *models.ResponseScheme, error) { + endpoint := "/wiki/rest/api/template/" + templateID + + request, err := i.c.NewRequest(ctx, http.MethodGet, endpoint, "", nil) + if err != nil { + return nil, nil, err + } + + result := new(models.ContentTemplateScheme) + response, err := i.c.Call(request, result) + if err != nil { + return nil, response, err + } + + return result, response, nil +} diff --git a/confluence/internal/template_impl_test.go b/confluence/internal/template_impl_test.go new file mode 100644 index 00000000..d2aee483 --- /dev/null +++ b/confluence/internal/template_impl_test.go @@ -0,0 +1,380 @@ +package internal + +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ctreminiom/go-atlassian/pkg/infra/models" + "github.com/ctreminiom/go-atlassian/service" + "github.com/ctreminiom/go-atlassian/service/mocks" +) + +func Test_internalTemplateImpl_Create(t *testing.T) { + type fields struct { + c service.Connector + } + type args struct { + ctx context.Context + payload *models.CreateTemplateScheme + } + tests := []struct { + name string + fields fields + args args + on func(*fields) + wantErr bool + Err error + }{ + { + name: "CreateTemplateScheme.Create returned error", + args: args{ + ctx: context.Background(), + payload: &models.CreateTemplateScheme{ + Name: "Test Template", + TemplateType: "page", + Body: &models.ContentTemplateBodyCreateScheme{ + View: &models.ContentBodyCreateScheme{ + Value: "

Test Template

", + Representation: "storage", + }, + }, + Description: "This is a test template", + Space: &models.SpaceScheme{ + Key: "TEST", + }, + }, + }, + on: func(fields *fields) { + client := mocks.NewConnector(t) + + client.On("NewRequest", + context.Background(), + http.MethodPost, + "/wiki/rest/api/template", + "", + &models.CreateTemplateScheme{ + Name: "Test Template", + TemplateType: "page", + Body: &models.ContentTemplateBodyCreateScheme{ + View: &models.ContentBodyCreateScheme{ + Value: "

Test Template

", + Representation: "storage", + }, + }, + Description: "This is a test template", + Space: &models.SpaceScheme{ + Key: "TEST", + }, + }, + ). + Return(&http.Request{}, errors.New("error, unable to create the http request")) + + fields.c = client + }, + wantErr: true, + Err: errors.New("error, unable to create the http request"), + }, + { + name: "CreateTemplateScheme.Create success", + args: args{ + ctx: context.Background(), + payload: &models.CreateTemplateScheme{ + Name: "Test Template", + TemplateType: "page", + Body: &models.ContentTemplateBodyCreateScheme{ + View: &models.ContentBodyCreateScheme{ + Value: "

Test Template

", + Representation: "storage", + }, + }, + Description: "This is a test template", + Space: &models.SpaceScheme{ + Key: "TEST", + }, + }, + }, + on: func(fields *fields) { + client := mocks.NewConnector(t) + + client.On("NewRequest", + context.Background(), + http.MethodPost, + "/wiki/rest/api/template", + "", + &models.CreateTemplateScheme{ + Name: "Test Template", + TemplateType: "page", + Body: &models.ContentTemplateBodyCreateScheme{ + View: &models.ContentBodyCreateScheme{ + Value: "

Test Template

", + Representation: "storage", + }, + }, + Description: "This is a test template", + Space: &models.SpaceScheme{ + Key: "TEST", + }, + }, + ). + Return(&http.Request{}, nil) + + client.On("Call", &http.Request{}, &models.ContentTemplateScheme{}). + Return(&models.ResponseScheme{Code: 200}, nil) + + fields.c = client + }, + wantErr: false, + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + if testCase.on != nil { + testCase.on(&testCase.fields) + } + + newService := NewTemplateService(testCase.fields.c) + + gotResult, gotResponse, err := newService.Create(testCase.args.ctx, testCase.args.payload) + + if testCase.wantErr { + assert.EqualError(t, err, testCase.Err.Error()) + } else { + assert.NoError(t, err) + assert.NotEqual(t, gotResponse, nil) + assert.NotEqual(t, gotResult, nil) + } + }) + } +} + +func Test_internalTemplateImpl_Update(t *testing.T) { + type fields struct { + c service.Connector + } + type args struct { + ctx context.Context + payload *models.UpdateTemplateScheme + } + tests := []struct { + name string + fields fields + args args + on func(*fields) + wantErr bool + Err error + }{ + { + name: "CreateTemplateScheme.Create returned error", + args: args{ + ctx: context.Background(), + payload: &models.UpdateTemplateScheme{ + TemplateID: "1234567", + Name: "Test Template", + TemplateType: "page", + Body: &models.ContentTemplateBodyCreateScheme{ + View: &models.ContentBodyCreateScheme{ + Value: "

Test Template

", + Representation: "storage", + }, + }, + Description: "This is a test template", + Space: &models.SpaceScheme{ + Key: "TEST", + }, + }, + }, + on: func(fields *fields) { + client := mocks.NewConnector(t) + + client.On("NewRequest", + context.Background(), + http.MethodPut, + "/wiki/rest/api/template", + "", + &models.UpdateTemplateScheme{ + TemplateID: "1234567", + Name: "Test Template", + TemplateType: "page", + Body: &models.ContentTemplateBodyCreateScheme{ + View: &models.ContentBodyCreateScheme{ + Value: "

Test Template

", + Representation: "storage", + }, + }, + Description: "This is a test template", + Space: &models.SpaceScheme{ + Key: "TEST", + }, + }, + ). + Return(&http.Request{}, errors.New("error, unable to create the http request")) + + fields.c = client + }, + wantErr: true, + Err: errors.New("error, unable to create the http request"), + }, + { + name: "CreateTemplateScheme.Create success", + args: args{ + ctx: context.Background(), + payload: &models.UpdateTemplateScheme{ + TemplateID: "123456789", + Name: "Test Template", + TemplateType: "page", + Body: &models.ContentTemplateBodyCreateScheme{ + View: &models.ContentBodyCreateScheme{ + Value: "

Test Template

", + Representation: "storage", + }, + }, + Description: "This is a test template", + Space: &models.SpaceScheme{ + Key: "TEST", + }, + }, + }, + on: func(fields *fields) { + client := mocks.NewConnector(t) + + client.On("NewRequest", + context.Background(), + http.MethodPut, + "/wiki/rest/api/template", + "", + &models.UpdateTemplateScheme{ + TemplateID: "123456789", + Name: "Test Template", + TemplateType: "page", + Body: &models.ContentTemplateBodyCreateScheme{ + View: &models.ContentBodyCreateScheme{ + Value: "

Test Template

", + Representation: "storage", + }, + }, + Description: "This is a test template", + Space: &models.SpaceScheme{ + Key: "TEST", + }, + }, + ). + Return(&http.Request{}, nil) + + client.On("Call", &http.Request{}, &models.ContentTemplateScheme{}). + Return(&models.ResponseScheme{Code: 200}, nil) + + fields.c = client + }, + wantErr: false, + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + if testCase.on != nil { + testCase.on(&testCase.fields) + } + + newService := NewTemplateService(testCase.fields.c) + + gotResult, gotResponse, err := newService.Update(testCase.args.ctx, testCase.args.payload) + + if testCase.wantErr { + assert.EqualError(t, err, testCase.Err.Error()) + } else { + assert.NoError(t, err) + assert.NotEqual(t, gotResponse, nil) + assert.NotEqual(t, gotResult, nil) + } + }) + } +} + +func Test_internalTemplateImpl_Get(t *testing.T) { + type fields struct { + c service.Connector + } + type args struct { + ctx context.Context + templateID string + } + tests := []struct { + name string + fields fields + args args + on func(*fields) + wantErr bool + Err error + }{ + { + name: "CreateTemplateScheme.Create returned error", + args: args{ + ctx: context.Background(), + templateID: "123456", + }, + on: func(fields *fields) { + client := mocks.NewConnector(t) + + client.On("NewRequest", + context.Background(), + http.MethodGet, + "/wiki/rest/api/template/123456", + "", + nil, + ). + Return(&http.Request{}, errors.New("error, unable to create the http request")) + + fields.c = client + }, + wantErr: true, + Err: errors.New("error, unable to create the http request"), + }, + { + name: "CreateTemplateScheme.Create success", + args: args{ + ctx: context.Background(), + templateID: "123456789", + }, + on: func(fields *fields) { + client := mocks.NewConnector(t) + + client.On("NewRequest", + context.Background(), + http.MethodGet, + "/wiki/rest/api/template/123456789", + "", + nil, + ). + Return(&http.Request{}, nil) + + client.On("Call", &http.Request{}, &models.ContentTemplateScheme{}). + Return(&models.ResponseScheme{Code: 200}, nil) + + fields.c = client + }, + wantErr: false, + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + if testCase.on != nil { + testCase.on(&testCase.fields) + } + + newService := NewTemplateService(testCase.fields.c) + + gotResult, gotResponse, err := newService.Get(testCase.args.ctx, testCase.args.templateID) + + if testCase.wantErr { + assert.EqualError(t, err, testCase.Err.Error()) + } else { + assert.NoError(t, err) + assert.NotEqual(t, gotResponse, nil) + assert.NotEqual(t, gotResult, nil) + } + }) + } +} diff --git a/pkg/infra/models/confluence_template.go b/pkg/infra/models/confluence_template.go new file mode 100644 index 00000000..0ed9a1d4 --- /dev/null +++ b/pkg/infra/models/confluence_template.go @@ -0,0 +1,109 @@ +package models + +// UpdateTemplateScheme represents a Confluence template update. +type UpdateTemplateScheme struct { + // TemplateID the ID of the template being updated. + TemplateID string `json:"templateId"` + // Name of the template. Set to the current name if this field is not being updated. + Name string `json:"name"` + // TemplateType of the template. + TemplateType string `json:"templateType"` + // Body of the new content. + Body *ContentTemplateBodyCreateScheme `json:"body"` + // Description of the template. + // Max length: 100 + Description string `json:"description"` + // Labels to add to the template. + Labels []LabelValueScheme `json:"labels,omitempty"` + // Space the template is in. + // Only the Space.Key is required. + Space *SpaceScheme `json:"space"` +} + +// CreateTemplateScheme represents a Confluence template creation. +type CreateTemplateScheme struct { + // Name of the template. Set to the current name if this field is not being updated. + Name string `json:"name"` + // TemplateType of the template. + TemplateType string `json:"templateType"` + // Body of the new content. + Body *ContentTemplateBodyCreateScheme `json:"body"` + // Description of the template. + // Max length: 255 + Description string `json:"description"` + // Labels to add to the template. + Labels []LabelValueScheme `json:"labels,omitempty"` + // Space the template is in. + // Only the Space.Key is required. + Space *SpaceScheme `json:"space"` +} + +// ContentTemplateBodySchema is the body of the template. +type ContentTemplateBodySchema struct { + View *ContentBodyCreateScheme `json:"view,omitempty"` + ExportView *ContentBodyCreateScheme `json:"export_view,omitempty"` + StyledView *ContentBodyCreateScheme `json:"styled_view,omitempty"` + Storage *ContentBodyCreateScheme `json:"storage,omitempty"` + Editor *ContentBodyCreateScheme `json:"editor,omitempty"` + Editor2 *ContentBodyCreateScheme `json:"editor2,omitempty"` + Wiki *ContentBodyCreateScheme `json:"wiki,omitempty"` + AtlasDocFormat *ContentBodyCreateScheme `json:"atlas_doc_format,omitempty"` + AnonymousExportView *ContentBodyCreateScheme `json:"anonymous_export_view,omitempty"` +} + +// ContentBodyScheme is used when creating or updating content. +type ContentBodyScheme struct { + // The body of the content in the relevant format. + Value string `json:"value"` + // The content format type. Set the value of this property to the name of the format being used, e.g. 'storage'. + // Valid values: view, export_view, styled_view, storage, editor, editor2, anonymous_export_view, wiki, atlas_doc_format, plain, raw + Representation string `json:"representation"` +} + +// ContentTemplateBodyCreateScheme is the body of the template for creating. +// Only one body format should be specified as the property for this object, e.g. storage. +// Note, editor2 format is used by Atlassian only. anonymous_export_view is the same as export_view format but only content viewable by an anonymous user is included. +type ContentTemplateBodyCreateScheme struct { + View *ContentBodyCreateScheme `json:"view,omitempty"` + ExportView *ContentBodyCreateScheme `json:"export_view,omitempty"` + StyledView *ContentBodyCreateScheme `json:"styled_view,omitempty"` + Storage *ContentBodyCreateScheme `json:"storage,omitempty"` + Editor *ContentBodyCreateScheme `json:"editor,omitempty"` + Editor2 *ContentBodyCreateScheme `json:"editor2,omitempty"` + Wiki *ContentBodyCreateScheme `json:"wiki,omitempty"` + AtlasDocFormat *ContentBodyCreateScheme `json:"atlas_doc_format,omitempty"` + AnonymousExportView *ContentBodyCreateScheme `json:"anonymous_export_view,omitempty"` +} + +// ContentBodyCreateScheme is used when creating or updating content. +type ContentBodyCreateScheme struct { + // The body of the content in the relevant format. + Value string `json:"value"` + // The content format type. Set the value of this property to the name of the format being used, e.g. 'storage'. + // Valid values: view, export_view, styled_view, storage, editor, editor2, anonymous_export_view, wiki, atlas_doc_format, plain, raw + Representation string `json:"representation"` +} + +type ContentTemplateScheme struct { + TemplateID string `json:"templateId"` + OriginalTemplate *OriginalTemplateScheme `json:"originalTemplate,omitempty"` + ReferencingBlueprint string `json:"referencingBlueprint"` + Name string `json:"name"` + Description string `json:"description"` + // Labels to add to the template. + Labels []LabelValueScheme `json:"labels"` + // TemplateType of the template. + TemplateType string `json:"templateType"` + EditorVersion string `json:"editorVersion"` + Body *ContentTemplateBodySchema `json:"body"` + Expandable *ContentTemplateExpandableScheme `json:"_expandable,omitempty"` +} + +type OriginalTemplateScheme struct { + PluginKey string `json:"pluginKey"` + ModuleKey string `json:"moduleKey"` +} + +type ContentTemplateExpandableScheme struct { + Body string `json:"body"` +} diff --git a/service/confluence/template.go b/service/confluence/template.go new file mode 100644 index 00000000..f9a07cd5 --- /dev/null +++ b/service/confluence/template.go @@ -0,0 +1,30 @@ +package confluence + +import ( + "context" + + "github.com/ctreminiom/go-atlassian/pkg/infra/models" +) + +type TemplateConnector interface { + // Update updates a template. + // + // PUT /wiki/rest/api/template + // + // https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-template/#api-wiki-rest-api-template-put + Update(ctx context.Context, payload *models.UpdateTemplateScheme) (*models.ContentTemplateScheme, *models.ResponseScheme, error) + + // Create creates a new template. + // + // POST /wiki/rest/api/template + // + // https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-template/#api-wiki-rest-api-template-post + Create(ctx context.Context, payload *models.CreateTemplateScheme) (*models.ContentTemplateScheme, *models.ResponseScheme, error) + + // Get content template by ID. + // + // GET /wiki/rest/api/template/{id} + // + // https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-template/#api-wiki-rest-api-template-contenttemplateid-get + Get(ctx context.Context, templateID string) (*models.ContentTemplateScheme, *models.ResponseScheme, error) +}