diff --git a/.gitignore b/.gitignore index 576f4be..3fa6c9b 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ vendor/ .vscode/ .env +ex/ diff --git a/.golangci.yml b/.golangci.yml index 0589459..425e497 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,19 +1,37 @@ linters: disable-all: true enable: + - asciicheck + - bodyclose + - contextcheck - deadcode - - gofmt + - durationcheck + - errname - gomnd - gosimple + - govet - ineffassign - makezero - misspell - nakedret - nilerr + - noctx - nolintlint + - revive - staticcheck - structcheck + - typecheck - unconvert - unparam + - unused - varcheck - vet + - wastedassign +linters-settings: + revive: + ignore-generated-header: true + severity: warning + rules: + - name: atomic + - name: var-naming + disabled: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 80ad71a..cef4686 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,9 +23,8 @@ repos: rev: v0.5.0 hooks: - id: go-fmt - - id: go-vet - - id: golangci-lint - id: go-mod-tidy + - id: golangci-lint - repo: https://github.com/antonbabenko/pre-commit-terraform rev: v1.64.0 hooks: diff --git a/docs/data-sources/article.md b/docs/data-sources/article.md index 1af86da..11c5f77 100644 --- a/docs/data-sources/article.md +++ b/docs/data-sources/article.md @@ -34,9 +34,9 @@ data "forem_article" "example_username_slug" { ### Optional -- `id` (String) ID of the article. -- `slug` (String) Slug of the article. -- `username` (String) User or organization username. +- `id` (String) ID of the article. At least one of the following has to be added: `id, username, slug`. +- `slug` (String) Slug of the article. At least one of the following has to be added: `id, username, slug`. Required to be set with the following: `username`. +- `username` (String) User or organization username. At least one of the following has to be added: `id, username, slug`. Required to be set with the following: `slug`. ### Read-Only diff --git a/docs/index.md b/docs/index.md index c1eda1f..fd8d04a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -25,5 +25,5 @@ provider "forem" { ### Optional -- `api_key` (String) API key to be able to communicate with the FOREM API. Can be specified with the `FOREM_API_KEY` environment variable. -- `host` (String) Host of the FOREM API. You can specify the `dev.to` or any other Forem installation. Can be specified with the `FOREM_HOST` environment variable. Default: `https://dev.to/api`. +- `api_key` (String) API key to be able to communicate with the FOREM API. Environment variable: `FOREM_API_KEY`. +- `host` (String) Host of the FOREM API. Environment variable: `FOREM_HOST`. Defaults to: `https://dev.to/api`. diff --git a/docs/resources/article.md b/docs/resources/article.md index 7b3a6c4..7a00796 100644 --- a/docs/resources/article.md +++ b/docs/resources/article.md @@ -89,9 +89,9 @@ resource "forem_article" "example_full" { - `cover_image` (String) URL of the cover image of the article. - `description` (String) Article description. - `organization_id` (Number) Only users belonging to an organization can assign the article to it. -- `published` (Boolean) Set to `true` to create a published article. Defaults to `false`. +- `published` (Boolean) Set to `true` to create a published article. Defaults to: `false`. - `series` (String) Article series name. All articles belonging to the same series need to have the same name in this parameter. -- `tags` (List of String) List of tags related to the article. +- `tags` (List of String) List of tags related to the article. Maximum items: `4`. ### Read-Only diff --git a/docs/resources/listing.md b/docs/resources/listing.md new file mode 100644 index 0000000..6610fdd --- /dev/null +++ b/docs/resources/listing.md @@ -0,0 +1,50 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "forem_listing Resource - terraform-provider-forem" +subcategory: "" +description: |- + forem_listing resource creates and updates a particular listing. A listing is a classified ad that users create on Forem. They can be related to conference announcements, job offers, mentorships, upcoming events and more. + API Docs + https://developers.forem.com/api#operation/createListinghttps://developers.forem.com/api#operation/updateListing +--- + +# forem_listing (Resource) + +`forem_listing` resource creates and updates a particular listing. A listing is a classified ad that users create on Forem. They can be related to conference announcements, job offers, mentorships, upcoming events and more. + +## API Docs + +- https://developers.forem.com/api#operation/createListing +- https://developers.forem.com/api#operation/updateListing + + + + +## Schema + +### Required + +- `body_markdown` (String) The body of the listing in Markdown format. +- `category` (String) The category that the listing belongs to. +- `title` (String) Title of the listing. + +### Optional + +- `action` (String) Set it to `draft` to create an unpublished listing. +- `contact_via_connect` (Boolean) True if users are allowed to contact the listing's owner via DEV connect, false otherwise. Defaults to: `false`. +- `expires_at` (String) Date and time of expiration. +- `location` (String) Geographical area or city for the listing. +- `organization_id` (Number) The id of the organization the user is creating the listing for. Only users belonging to an organization can assign the listing to it. +- `tags` (List of String) List of tags related to the listing. Maximum items: `8`. + +### Read-Only + +- `created_at` (String) When the listing was created. +- `id` (String) ID of the listing. +- `organization` (Map of String) Organization object of the listing. +- `published` (Boolean) Whether the listing is published or not. +- `slug` (String) Slug of the listing. +- `updated_at` (String) When the listing was updated. +- `user` (Map of String) User object of the listing. + + diff --git a/forem/data_source_followed_tags.go b/forem/data_source_followed_tags.go index 76ef6e0..b6bc948 100644 --- a/forem/data_source_followed_tags.go +++ b/forem/data_source_followed_tags.go @@ -12,7 +12,7 @@ import ( ) const ( - FormatIntBase = 10 + formatIntBase = 10 ) func dataSourceFollowedTags() *schema.Resource { @@ -71,7 +71,7 @@ func dataSourceFollowedTagsRead(ctx context.Context, d *schema.ResourceData, met } d.Set("tags", ftags) - d.SetId(strconv.FormatInt(time.Now().Unix(), FormatIntBase)) + d.SetId(strconv.FormatInt(time.Now().Unix(), formatIntBase)) return nil } diff --git a/forem/provider.go b/forem/provider.go index efcf800..4bb3736 100644 --- a/forem/provider.go +++ b/forem/provider.go @@ -11,7 +11,10 @@ import ( ) const ( - DEV_TO_BASE_URL = "https://dev.to/api" + devToBaseURL = "https://dev.to/api" + + envForemAPIKey = "FOREM_API_KEY" + envForemHost = "FOREM_HOST" ) func init() { @@ -19,11 +22,26 @@ func init() { schema.SchemaDescriptionBuilder = func(s *schema.Schema) string { desc := s.Description if s.Default != nil { - desc += fmt.Sprintf(" Defaults to `%v`.", s.Default) + desc += fmt.Sprintf(" Defaults to: `%v`.", s.Default) } if s.Deprecated != "" { desc += " " + s.Deprecated } + if len(s.AtLeastOneOf) > 0 { + desc += fmt.Sprintf(" At least one of the following has to be added: `%s`.", strings.Join(s.AtLeastOneOf, ", ")) + } + if len(s.ConflictsWith) > 0 { + desc += fmt.Sprintf(" Conflicts with the following: `%s`.", strings.Join(s.ConflictsWith, ", ")) + } + if len(s.RequiredWith) > 0 { + desc += fmt.Sprintf(" Required to be set with the following: `%s`.", strings.Join(s.RequiredWith, ", ")) + } + if s.MinItems > 0 { + desc += fmt.Sprintf(" Minimum items: `%d`.", s.MinItems) + } + if s.MaxItems > 0 { + desc += fmt.Sprintf(" Maximum items: `%d`.", s.MaxItems) + } return strings.TrimSpace(desc) } } @@ -34,20 +52,21 @@ func Provider() *schema.Provider { ConfigureContextFunc: providerConfigure, Schema: map[string]*schema.Schema{ "api_key": { - Description: "API key to be able to communicate with the FOREM API. Can be specified with the `FOREM_API_KEY` environment variable.", + Description: fmt.Sprintf("API key to be able to communicate with the FOREM API. Environment variable: `%s`.", envForemAPIKey), Type: schema.TypeString, Required: true, - DefaultFunc: schema.EnvDefaultFunc("FOREM_API_KEY", nil), + DefaultFunc: schema.EnvDefaultFunc(envForemAPIKey, nil), }, "host": { - Description: "Host of the FOREM API. You can specify the `dev.to` or any other Forem installation. Can be specified with the `FOREM_HOST` environment variable. Default: `https://dev.to/api`.", + Description: fmt.Sprintf("Host of the FOREM API. Environment variable: `%s`. Defaults to: `%s`.", envForemHost, devToBaseURL), Type: schema.TypeString, Required: true, - DefaultFunc: schema.EnvDefaultFunc("FOREM_HOST", DEV_TO_BASE_URL), + DefaultFunc: schema.EnvDefaultFunc(envForemHost, devToBaseURL), }, }, ResourcesMap: map[string]*schema.Resource{ "forem_article": resourceArticle(), + "forem_listing": resourceListing(), }, DataSourcesMap: map[string]*schema.Resource{ "forem_user": dataSourceUser(), diff --git a/forem/resource_article.go b/forem/resource_article.go index 7c59080..a1b1024 100644 --- a/forem/resource_article.go +++ b/forem/resource_article.go @@ -13,8 +13,8 @@ import ( ) const ( - MaxArticleTags = 4 - ReadArticlesPerPage = 25 + maxArticleTags = 4 + readArticlesPerPage = 25 ) func resourceArticle() *schema.Resource { @@ -55,7 +55,7 @@ func resourceArticle() *schema.Resource { Description: "List of tags related to the article.", Type: schema.TypeList, Optional: true, - MaxItems: MaxArticleTags, + MaxItems: maxArticleTags, Elem: &schema.Schema{Type: schema.TypeString}, }, "published": { @@ -166,6 +166,7 @@ func resourceArticle() *schema.Resource { } } +// TODO: Waiting for API to allow deletion of an article func resourceArticleDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { return nil } @@ -243,12 +244,11 @@ func resourceArticleUpdate(ctx context.Context, d *schema.ResourceData, meta int ab.Article.CanonicalURL = v.(string) } if v, ok := d.GetOk("tags"); ok { - tags := v.([]interface{}) - tagsList := []string{} - for _, t := range tags { - tagsList = append(tagsList, t.(string)) + tags := []string{} + for _, t := range v.([]interface{}) { + tags = append(tags, t.(string)) } - ab.Article.Tags = tagsList + ab.Article.Tags = tags } if v, ok := d.GetOk("organization_id"); ok { ab.Article.OrganizationID = v.(int32) @@ -266,10 +266,10 @@ func resourceArticleRead(ctx context.Context, d *schema.ResourceData, meta inter client := meta.(*dev.Client) id := d.Get("id").(string) - tflog.Debug(ctx, fmt.Sprintf("Getting article: %s", id)) + tflog.Debug(ctx, fmt.Sprintf("Getting article with ID: %s", id)) page := int32(1) - perPage := int32(ReadArticlesPerPage) + perPage := int32(readArticlesPerPage) missing := true var article dev.Article @@ -285,7 +285,7 @@ func resourceArticleRead(ctx context.Context, d *schema.ResourceData, meta inter for _, v := range articleResp { if strconv.Itoa(int(v.ID)) == id { - tflog.Debug(ctx, fmt.Sprintf("Found article: %s", id)) + tflog.Debug(ctx, fmt.Sprintf("Found article with ID: %s", id)) missing = false article = v break diff --git a/forem/resource_article_test.go b/forem/resource_article_test.go index 2c5b49e..b78b296 100644 --- a/forem/resource_article_test.go +++ b/forem/resource_article_test.go @@ -18,18 +18,18 @@ func TestAccArticle_basic(t *testing.T) { resourceName := "forem_article.test" title := gofakeit.SentenceSimple() - body_markdown := gofakeit.HipsterParagraph(2, 5, 10, "\n") + bodyMarkdown := gofakeit.HipsterParagraph(2, 5, 10, "\n") resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccArticleBasicConfig(title, body_markdown), + Config: testAccArticleBasicConfig(title, bodyMarkdown), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrSet(resourceName, "id"), resource.TestCheckResourceAttr(resourceName, "title", title), - resource.TestCheckResourceAttr(resourceName, "body_markdown", body_markdown), + resource.TestCheckResourceAttr(resourceName, "body_markdown", bodyMarkdown), resource.TestCheckResourceAttr(resourceName, "published", "false"), resource.TestCheckResourceAttr(resourceName, "tags.#", "0"), resource.TestCheckResourceAttrSet(resourceName, "slug"), @@ -46,7 +46,7 @@ func TestAccArticle_full(t *testing.T) { title := gofakeit.HipsterSentence(5) published := gofakeit.Bool() - canonical_url := gofakeit.URL() + canonicalURL := gofakeit.URL() tags := []string{gofakeit.Word(), gofakeit.Word(), gofakeit.Word()} series := gofakeit.LoremIpsumSentence(3) article := &dev.Article{ @@ -54,7 +54,7 @@ func TestAccArticle_full(t *testing.T) { BodyMarkdown: gofakeit.HipsterParagraph(2, 5, 10, "\n"), Published: published, Description: gofakeit.LoremIpsumSentence(5), - CanonicalURL: canonical_url, + CanonicalURL: canonicalURL, CoverImage: gofakeit.URL(), TagList: tags, } @@ -64,7 +64,7 @@ func TestAccArticle_full(t *testing.T) { BodyMarkdown: gofakeit.HipsterParagraph(2, 5, 10, "\n"), Published: published, Description: gofakeit.LoremIpsumSentence(5), - CanonicalURL: canonical_url, + CanonicalURL: canonicalURL, CoverImage: gofakeit.URL(), TagList: append(tags, gofakeit.Word()), } @@ -135,19 +135,19 @@ func TestAccArticle_tooManyTags(t *testing.T) { }) } -func testAccArticleBasicConfig(title, body_markdown string) string { +func testAccArticleBasicConfig(title, bodyMarkdown string) string { return fmt.Sprintf(` resource "forem_article" "test" { - title = %[1]q - body_markdown = %[2]q - }`, title, body_markdown) + title = %q + body_markdown = %q +}`, title, bodyMarkdown) } func testAccArticleFullConfig(article *dev.Article, series string) string { return fmt.Sprintf(` resource "forem_article" "test" { - title = %[1]q - body_markdown = %[2]q + title = %q + body_markdown = %q published = %v description = %q canonical_url = %q diff --git a/forem/resource_listing.go b/forem/resource_listing.go new file mode 100644 index 0000000..48d5f89 --- /dev/null +++ b/forem/resource_listing.go @@ -0,0 +1,228 @@ +package forem + +import ( + "context" + "fmt" + "regexp" + "strconv" + "terraform-provider-forem/internal/listing" + "time" + + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + dev "github.com/karvounis/dev-client-go" +) + +const ( + maxListingTags = 8 + validListingExpiresAtFormat = `\d{2}\/\d{2}\/\d{4}` +) + +var ( + allowedListingActions = []string{string(dev.ActionDraft), string(dev.ActionBump), string(dev.ActionPublish), string(dev.ActionUnpublish)} + allowedListingCategories = []string{ + string(dev.ListingCategoryCfp), + string(dev.ListingCategoryForhire), + string(dev.ListingCategoryCollabs), + string(dev.ListingCategoryEducation), + string(dev.ListingCategoryJobs), + string(dev.ListingCategoryMentors), + string(dev.ListingCategoryProducts), + string(dev.ListingCategoryMentees), + string(dev.ListingCategoryForsale), + string(dev.ListingCategoryEvents), + string(dev.ListingCategoryMisc), + } +) + +func resourceListing() *schema.Resource { + return &schema.Resource{ + Description: "`forem_listing` resource creates and updates a particular listing. A listing is a classified ad that users create on Forem. They can be related to conference announcements, job offers, mentorships, upcoming events and more." + + "\n\n## API Docs\n\n" + + "- https://developers.forem.com/api#operation/createListing\n" + + "- https://developers.forem.com/api#operation/updateListing", + ReadContext: resourceListingRead, + CreateContext: resourceListingCreate, + UpdateContext: resourceListingUpdate, + DeleteContext: resourceListingDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "id": { + Description: "ID of the listing.", + Type: schema.TypeString, + Computed: true, + }, + "title": { + Description: "Title of the listing.", + Type: schema.TypeString, + Required: true, + }, + "body_markdown": { + Description: "The body of the listing in Markdown format.", + Type: schema.TypeString, + Required: true, + }, + "category": { + Description: "The category that the listing belongs to.", + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(allowedListingCategories, false), + }, + "tags": { + Description: "List of tags related to the listing.", + Type: schema.TypeList, + Optional: true, + MaxItems: maxListingTags, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "contact_via_connect": { + Description: "True if users are allowed to contact the listing's owner via DEV connect, false otherwise.", + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "expires_at": { + Description: "Date and time of expiration.", + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringMatch(regexp.MustCompile(validListingExpiresAtFormat), fmt.Sprintf("expected to be of the format `%s`", validListingExpiresAtFormat)), + }, + "location": { + Description: "Geographical area or city for the listing.", + Type: schema.TypeString, + Optional: true, + }, + "action": { + Description: "Set it to `draft` to create an unpublished listing.", + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(allowedListingActions, false), + }, + "organization_id": { + Description: "The id of the organization the user is creating the listing for. Only users belonging to an organization can assign the listing to it.", + Type: schema.TypeInt, + Optional: true, + }, + "slug": { + Description: "Slug of the listing.", + Type: schema.TypeString, + Computed: true, + }, + "published": { + Description: "Whether the listing is published or not.", + Type: schema.TypeBool, + Computed: true, + }, + "user": { + Description: "User object of the listing.", + Type: schema.TypeMap, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "organization": { + Description: "Organization object of the listing.", + Type: schema.TypeMap, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "created_at": { + Description: "When the listing was created.", + Type: schema.TypeString, + Computed: true, + }, + "updated_at": { + Description: "When the listing was updated.", + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +// TODO: Waiting for API to allow deletion of a listing +func resourceListingDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return nil +} + +func resourceListingCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*dev.Client) + + lbc := listing.GetListingBodySchemaFromResourceData(d) + tflog.Debug(ctx, fmt.Sprintf("Creating listing with title `%s` and category `%s`", lbc.Listing.Title, lbc.Listing.Category)) + resp, err := client.CreateListing(lbc, nil) + if err != nil { + return diag.FromErr(err) + } + tflog.Debug(ctx, fmt.Sprintf("Created listing with ID: %d", resp.ID)) + + d.SetId(strconv.Itoa(int(resp.ID))) + d.Set("created_at", time.Now().Format(time.RFC3339)) + + return resourceListingRead(ctx, d, meta) +} + +func resourceListingUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*dev.Client) + + tflog.Debug(ctx, fmt.Sprintf("Updating listing with ID: %s", d.Id())) + lbc := listing.GetListingBodySchemaFromResourceData(d) + if _, err := client.UpdateListing(d.Id(), lbc, nil); err != nil { + return diag.FromErr(err) + } + tflog.Debug(ctx, fmt.Sprintf("Updated listing with ID: %s", d.Id())) + + d.Set("updated_at", time.Now().Format(time.RFC3339)) + + return resourceListingRead(ctx, d, meta) +} + +func resourceListingRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*dev.Client) + + id := d.Get("id").(string) + tflog.Debug(ctx, fmt.Sprintf("Getting listing with ID: %s", id)) + resp, err := client.GetListingByID(id) + if err != nil { + return diag.FromErr(err) + } + tflog.Debug(ctx, fmt.Sprintf("Found listing with ID: %s", id)) + + d.SetId(id) + d.Set("title", resp.Title) + d.Set("body_markdown", resp.BodyMarkdown) + d.Set("slug", resp.Slug) + d.Set("category", resp.Category) + d.Set("published", resp.Published) + d.Set("tags", resp.Tags) + + if resp.User != nil { + d.Set("user", map[string]interface{}{ + "name": resp.User.Name, + "username": resp.User.Username, + "twitter_username": resp.User.TwitterUsername, + "github_username": resp.User.GithubUsername, + "website_url": resp.User.WebsiteURL, + "profile_image": resp.User.ProfileImage, + }) + } else { + d.Set("user", map[string]interface{}{}) + } + + if resp.Organization != nil { + d.Set("organization", map[string]interface{}{ + "name": resp.Organization.Name, + "username": resp.Organization.Username, + "slug": resp.Organization.Slug, + "profile_image": resp.Organization.ProfileImage, + "profile_image_90": resp.Organization.ProfileImage90, + }) + } else { + d.Set("organization", map[string]interface{}{}) + } + + return nil +} diff --git a/forem/resource_listing_test.go b/forem/resource_listing_test.go new file mode 100644 index 0000000..4385b44 --- /dev/null +++ b/forem/resource_listing_test.go @@ -0,0 +1,186 @@ +package forem_test + +import ( + "fmt" + "strconv" + "strings" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/karvounis/dev-client-go" +) + +func TestAccListing_draft(t *testing.T) { + gofakeit.Seed(time.Now().UnixNano()) + resourceName := "forem_listing.test" + lbc := getListingBodySchemaToPublish(dev.ActionDraft) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccListingDraft(lbc), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "title", lbc.Listing.Title), + resource.TestCheckResourceAttr(resourceName, "body_markdown", lbc.Listing.BodyMarkdown), + resource.TestCheckResourceAttr(resourceName, "category", string(lbc.Listing.Category)), + resource.TestCheckResourceAttr(resourceName, "published", "false"), + resource.TestCheckResourceAttr(resourceName, "contact_via_connect", "false"), + resource.TestCheckResourceAttr(resourceName, "action", string(dev.ActionDraft)), + resource.TestCheckResourceAttr(resourceName, "tags.#", strconv.Itoa(len(lbc.Listing.Tags))), + resource.TestCheckNoResourceAttr(resourceName, "expires_at"), + resource.TestCheckNoResourceAttr(resourceName, "location"), + resource.TestCheckResourceAttrSet(resourceName, "user.username"), + ), + }, + }, + }) +} + +func TestAccListing_publish(t *testing.T) { + gofakeit.Seed(time.Now().UnixNano()) + resourceName := "forem_listing.test" + lbc := getListingBodySchemaToPublish(dev.Action("")) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccListingPublish(lbc), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "title", lbc.Listing.Title), + resource.TestCheckResourceAttr(resourceName, "body_markdown", lbc.Listing.BodyMarkdown), + resource.TestCheckResourceAttr(resourceName, "category", string(lbc.Listing.Category)), + resource.TestCheckResourceAttr(resourceName, "published", "true"), + resource.TestCheckResourceAttr(resourceName, "contact_via_connect", strconv.FormatBool(lbc.Listing.ContactViaConnect)), + resource.TestCheckResourceAttr(resourceName, "tags.#", strconv.Itoa(len(lbc.Listing.Tags))), + resource.TestCheckResourceAttr(resourceName, "location", lbc.Listing.Location), + resource.TestCheckResourceAttr(resourceName, "expires_at", lbc.Listing.ExpiresAt), + resource.TestCheckResourceAttrSet(resourceName, "user.username"), + resource.TestCheckNoResourceAttr(resourceName, "action"), + ), + }, + }, + }) +} + +func TestAccListing_publishAndEdit(t *testing.T) { + gofakeit.Seed(time.Now().UnixNano()) + resourceName := "forem_listing.test" + lbc := getListingBodySchemaToPublish(dev.Action("")) + + lbcEdit := lbc + lbcEdit.Listing.Action = dev.ActionUnpublish + lbcEdit.Listing.Tags = append(lbcEdit.Listing.Tags, gofakeit.Word()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccListingPublish(lbc), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "title", lbc.Listing.Title), + resource.TestCheckResourceAttr(resourceName, "body_markdown", lbc.Listing.BodyMarkdown), + resource.TestCheckResourceAttr(resourceName, "category", string(lbc.Listing.Category)), + resource.TestCheckResourceAttr(resourceName, "published", "true"), + resource.TestCheckResourceAttr(resourceName, "contact_via_connect", strconv.FormatBool(lbc.Listing.ContactViaConnect)), + resource.TestCheckResourceAttr(resourceName, "tags.#", strconv.Itoa(len(lbc.Listing.Tags))), + resource.TestCheckResourceAttr(resourceName, "location", lbc.Listing.Location), + resource.TestCheckResourceAttr(resourceName, "expires_at", lbc.Listing.ExpiresAt), + resource.TestCheckResourceAttrSet(resourceName, "user.username"), + resource.TestCheckNoResourceAttr(resourceName, "action"), + resource.TestCheckResourceAttrSet(resourceName, "created_at"), + resource.TestCheckNoResourceAttr(resourceName, "updated_at"), + ), + }, + { + Config: testAccListingPublish(lbcEdit), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "title", lbcEdit.Listing.Title), + resource.TestCheckResourceAttr(resourceName, "body_markdown", lbcEdit.Listing.BodyMarkdown), + resource.TestCheckResourceAttr(resourceName, "category", string(lbcEdit.Listing.Category)), + resource.TestCheckResourceAttr(resourceName, "published", "true"), + resource.TestCheckResourceAttr(resourceName, "contact_via_connect", strconv.FormatBool(lbcEdit.Listing.ContactViaConnect)), + resource.TestCheckResourceAttr(resourceName, "tags.#", strconv.Itoa(len(lbcEdit.Listing.Tags))), + resource.TestCheckResourceAttr(resourceName, "location", lbcEdit.Listing.Location), + resource.TestCheckResourceAttr(resourceName, "expires_at", lbcEdit.Listing.ExpiresAt), + resource.TestCheckResourceAttrSet(resourceName, "user.username"), + resource.TestCheckResourceAttrSet(resourceName, "created_at"), + resource.TestCheckResourceAttrSet(resourceName, "updated_at"), + ), + }, + }, + }) +} + +func testAccListingDraft(lbc dev.ListingBodySchema) string { + return fmt.Sprintf(` +resource "forem_listing" "test" { + title = %q + body_markdown = %q + category = %q + action = %q + + tags = %s +}`, + lbc.Listing.Title, + lbc.Listing.BodyMarkdown, + lbc.Listing.Category, + lbc.Listing.Action, + strings.Join(strings.Split(fmt.Sprintf("%q\n", lbc.Listing.Tags), " "), ", "), + ) +} + +func testAccListingPublish(lbc dev.ListingBodySchema) string { + return fmt.Sprintf(` +resource "forem_listing" "test" { + title = %q + body_markdown = %q + category = %q + expires_at = %q + contact_via_connect = %t + location = %q + + tags = %s +}`, + lbc.Listing.Title, + lbc.Listing.BodyMarkdown, + lbc.Listing.Category, + lbc.Listing.ExpiresAt, + lbc.Listing.ContactViaConnect, + lbc.Listing.Location, + strings.Join(strings.Split(fmt.Sprintf("%q\n", lbc.Listing.Tags), " "), ", "), + ) +} + +func getListingBodySchemaToPublish(action dev.Action) dev.ListingBodySchema { + return dev.ListingBodySchema{ + Listing: struct { + Title string `json:"title"` + BodyMarkdown string `json:"body_markdown"` + Category dev.ListingCategory `json:"category"` + Tags []string `json:"tags"` + TagList string `json:"tag_list"` + ExpiresAt string `json:"expires_at"` + ContactViaConnect bool `json:"contact_via_connect"` + Location string `json:"location"` + OrganizationID int64 `json:"organization_id,omitempty"` + Action dev.Action `json:"action"` + }{ + Title: gofakeit.Sentence(5), + BodyMarkdown: gofakeit.Paragraph(1, 2, 5, "\n"), + Category: dev.ListingCategory(gofakeit.RandomString([]string{string(dev.ListingCategoryCfp), string(dev.ListingCategoryEvents), string(dev.ListingCategoryMisc)})), + Tags: []string{gofakeit.Word(), gofakeit.Word(), gofakeit.Word()}, + ExpiresAt: gofakeit.DateRange(time.Now(), time.Now().AddDate(0, 0, gofakeit.IntRange(1, 10))).Format("02/01/2004"), + ContactViaConnect: gofakeit.Bool(), + Location: gofakeit.City(), + Action: action, + }, + } +} diff --git a/go.mod b/go.mod index 46def36..e131e9d 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/brianvoe/gofakeit/v6 v6.15.0 github.com/hashicorp/terraform-plugin-log v0.2.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.10.1 - github.com/karvounis/dev-client-go v1.1.1 + github.com/karvounis/dev-client-go v1.2.0 ) require ( diff --git a/go.sum b/go.sum index bf97b15..754eeb5 100644 --- a/go.sum +++ b/go.sum @@ -233,8 +233,8 @@ github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwA github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/karvounis/dev-client-go v1.1.1 h1:DP1GZe3g6F5b9MaWZzjDRzDsBLEfTirWr+bdce2sDas= -github.com/karvounis/dev-client-go v1.1.1/go.mod h1:NCxNSFLpa7x7cqvFnhZjNneVq+9jwBK+KwKc3SugyjQ= +github.com/karvounis/dev-client-go v1.2.0 h1:6OP7N+ky2t4cagPdoKlYKNJEYw/rlyat1zOnz879s0E= +github.com/karvounis/dev-client-go v1.2.0/go.mod h1:NCxNSFLpa7x7cqvFnhZjNneVq+9jwBK+KwKc3SugyjQ= github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck= github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= diff --git a/internal/listing/listing.go b/internal/listing/listing.go new file mode 100644 index 0000000..1f2ed0a --- /dev/null +++ b/internal/listing/listing.go @@ -0,0 +1,34 @@ +package listing + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/karvounis/dev-client-go" +) + +// GetListingBodySchemaFromResourceData generates a dev.ListingBodySchema object based on the values in the schema.ResourceData object +func GetListingBodySchemaFromResourceData(d *schema.ResourceData) dev.ListingBodySchema { + var lbc dev.ListingBodySchema + lbc.Listing.Title = d.Get("title").(string) + lbc.Listing.BodyMarkdown = d.Get("body_markdown").(string) + lbc.Listing.Category = dev.ListingCategory(d.Get("category").(string)) + if v, ok := d.GetOk("expires_at"); ok { + lbc.Listing.ExpiresAt = v.(string) + } + if v, ok := d.GetOk("contact_via_connect"); ok { + lbc.Listing.ContactViaConnect = v.(bool) + } + if v, ok := d.GetOk("location"); ok { + lbc.Listing.Location = v.(string) + } + if v, ok := d.GetOk("action"); ok { + lbc.Listing.Action = dev.Action(v.(string)) + } + if v, ok := d.GetOk("tags"); ok { + tags := []string{} + for _, t := range v.([]interface{}) { + tags = append(tags, t.(string)) + } + lbc.Listing.Tags = tags + } + return lbc +}