Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API v2 #377

Merged
merged 11 commits into from
Oct 23, 2023
8 changes: 8 additions & 0 deletions configs/config.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,14 @@ email {
from_address = "[email protected]"
}

// FeatureFlags contain available feature flags.
feature_flags {
// api_v2 enables v2 of the API.
flag "api_v2" {
enabled = false
}
}

// google_workspace configures Hermes to work with Google Workspace.
google_workspace {
// create_doc_shortcuts enables creating a shortcut in the shortcuts_folder
Expand Down
8 changes: 6 additions & 2 deletions internal/api/documents.go
Original file line number Diff line number Diff line change
Expand Up @@ -717,7 +717,7 @@ Hermes

// Summary.
if req.Summary != nil {
model.Summary = *req.Summary
model.Summary = req.Summary
}

// Title.
Expand All @@ -739,7 +739,11 @@ Hermes
}

w.WriteHeader(http.StatusOK)
l.Info("patched document", "doc_id", docID)
l.Info("patched document",
"method", r.Method,
"path", r.URL.Path,
"doc_id", docID,
)

// Compare Algolia and database documents to find data inconsistencies.
// Get document object from Algolia.
Expand Down
10 changes: 7 additions & 3 deletions internal/api/drafts.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ func DraftsHandler(
Name: req.Product,
},
Status: models.WIPDocumentStatus,
Summary: req.Summary,
Summary: &req.Summary,
Title: req.Title,
}
if err := model.Create(db); err != nil {
Expand Down Expand Up @@ -1104,7 +1104,7 @@ func DraftsDocumentHandler(
// Summary.
if req.Summary != nil {
doc.Summary = *req.Summary
model.Summary = *req.Summary
model.Summary = req.Summary
}

// Title.
Expand Down Expand Up @@ -1176,7 +1176,11 @@ func DraftsDocumentHandler(
fmt.Sprintf("[%s] %s", doc.DocNumber, doc.Title))

w.WriteHeader(http.StatusOK)
l.Info("patched draft document", "doc_id", docId)
l.Info("patched draft document",
"method", r.Method,
"path", r.URL.Path,
"doc_id", docId,
)

// Compare Algolia and database documents to find data inconsistencies.
// Get document object from Algolia.
Expand Down
63 changes: 55 additions & 8 deletions internal/api/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-multierror"
"github.com/iancoleman/strcase"
"github.com/stretchr/testify/assert"
)

// contains returns true if a string is present in a slice of strings.
Expand Down Expand Up @@ -115,6 +116,12 @@ func respondError(
http.Error(w, userErrMsg, httpCode)
}

// fakeT fulfills the assert.TestingT interface so we can use
// assert.ElementsMatch.
type fakeT struct{}

func (t fakeT) Errorf(string, ...interface{}) {}

// compareAlgoliaAndDatabaseDocument compares data for a document stored in
// Algolia and the database to determine any inconsistencies, which are returned
// back as a (multierror) error.
Expand Down Expand Up @@ -224,7 +231,7 @@ func compareAlgoliaAndDatabaseDocument(
dbApprovedBy = append(dbApprovedBy, r.User.EmailAddress)
}
}
if !reflect.DeepEqual(algoApprovedBy, dbApprovedBy) {
if !assert.ElementsMatch(fakeT{}, algoApprovedBy, dbApprovedBy) {
result = multierror.Append(result,
fmt.Errorf(
"approvedBy not equal, algolia=%v, db=%v",
Expand All @@ -242,7 +249,7 @@ func compareAlgoliaAndDatabaseDocument(
for _, a := range dbDoc.Approvers {
dbApprovers = append(dbApprovers, a.EmailAddress)
}
if !reflect.DeepEqual(algoApprovers, dbApprovers) {
if !assert.ElementsMatch(fakeT{}, algoApprovers, dbApprovers) {
result = multierror.Append(result,
fmt.Errorf(
"approvers not equal, algolia=%v, db=%v",
Expand All @@ -263,7 +270,7 @@ func compareAlgoliaAndDatabaseDocument(
dbChangesRequestedBy = append(dbChangesRequestedBy, r.User.EmailAddress)
}
}
if !reflect.DeepEqual(algoChangesRequestedBy, dbChangesRequestedBy) {
if !assert.ElementsMatch(fakeT{}, algoChangesRequestedBy, dbChangesRequestedBy) {
result = multierror.Append(result,
fmt.Errorf(
"changesRequestedBy not equal, algolia=%v, db=%v",
Expand All @@ -281,7 +288,7 @@ func compareAlgoliaAndDatabaseDocument(
for _, c := range dbDoc.Contributors {
dbContributors = append(dbContributors, c.EmailAddress)
}
if !reflect.DeepEqual(algoContributors, dbContributors) {
if !assert.ElementsMatch(fakeT{}, algoContributors, dbContributors) {
result = multierror.Append(result,
fmt.Errorf(
"contributors not equal, algolia=%v, db=%v",
Expand Down Expand Up @@ -353,7 +360,7 @@ func compareAlgoliaAndDatabaseDocument(
)
}

if !reflect.DeepEqual(algoCFVal, dbCFVal) {
if !assert.ElementsMatch(fakeT{}, algoCFVal, dbCFVal) {
result = multierror.Append(result,
fmt.Errorf(
"custom field %s not equal, algolia=%v, db=%v",
Expand All @@ -379,8 +386,24 @@ func compareAlgoliaAndDatabaseDocument(
"doc type %q not found", algoDocType))
}

// Compare file revisions.
// TODO: need to store this in the database first.
// Compare fileRevisions.
algoFileRevisions, err := getMapStringStringValue(algoDoc, "fileRevisions")
if err != nil {
result = multierror.Append(
result, fmt.Errorf("error getting fileRevisions value: %w", err))
} else {
dbFileRevisions := make(map[string]string)
for _, fr := range dbDoc.FileRevisions {
dbFileRevisions[fr.GoogleDriveFileRevisionID] = fr.Name
}
if !reflect.DeepEqual(algoFileRevisions, dbFileRevisions) {
result = multierror.Append(result,
fmt.Errorf(
"fileRevisions not equal, algolia=%v, db=%v",
algoFileRevisions, dbFileRevisions),
)
}
}

// Compare modifiedTime.
algoModifiedTime, err := getInt64Value(algoDoc, "modifiedTime")
Expand Down Expand Up @@ -473,7 +496,7 @@ func compareAlgoliaAndDatabaseDocument(
result, fmt.Errorf("error getting summary value: %w", err))
} else {
dbSummary := dbDoc.Summary
if algoSummary != dbSummary {
if dbSummary != nil && algoSummary != *dbSummary {
result = multierror.Append(result,
fmt.Errorf(
"summary not equal, algolia=%v, db=%v",
Expand Down Expand Up @@ -517,6 +540,30 @@ func getInt64Value(in map[string]any, key string) (int64, error) {
return result, nil
}

func getMapStringStringValue(in map[string]any, key string) (
map[string]string, error,
) {
result := make(map[string]string)

if v, ok := in[key]; ok {
if reflect.TypeOf(v).Kind() == reflect.Map {
for vk, vv := range v.(map[string]any) {
if vv, ok := vv.(string); ok {
result[vk] = vv
} else {
return nil, fmt.Errorf(
"invalid type: map value element is not a string")
}
}
return result, nil
} else {
return nil, fmt.Errorf("invalid type: value is not a map")
}
}

return result, nil
}

func getStringValue(in map[string]any, key string) (string, error) {
var result string

Expand Down
136 changes: 134 additions & 2 deletions internal/api/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ func TestCompareAlgoliaAndDatabaseDocument(t *testing.T) {
"createdTime": float64(time.Date(
2023, time.April, 5, 1, 0, 0, 0, time.UTC).Unix()),
"currentVersion": "1.2.3",
"fileRevisions": map[string]any{
"1": "FileRevision1",
"2": "FileRevision2",
},
"modifiedTime": float64(time.Date(
2023, time.April, 5, 23, 0, 0, 0, time.UTC).Unix()),
"owners": []any{
Expand Down Expand Up @@ -204,10 +208,20 @@ func TestCompareAlgoliaAndDatabaseDocument(t *testing.T) {
2023, time.April, 5, 1, 0, 0, 0, time.UTC),
DocumentModifiedAt: time.Date(
2023, time.April, 5, 23, 0, 0, 0, time.UTC),
FileRevisions: []models.DocumentFileRevision{
{
GoogleDriveFileRevisionID: "1",
Name: "FileRevision1",
},
{
GoogleDriveFileRevisionID: "2",
Name: "FileRevision2",
},
},
Owner: &models.User{
EmailAddress: "[email protected]",
},
Summary: "Summary1",
Summary: &[]string{"Summary1"}[0],
Status: models.InReviewDocumentStatus,
},
dbDocReviews: models.DocumentReviews{
Expand Down Expand Up @@ -270,6 +284,124 @@ func TestCompareAlgoliaAndDatabaseDocument(t *testing.T) {
},
},

"good with different order of slice and map fields": {
algoDoc: map[string]any{
"appCreated": true,
"approvedBy": []any{
"[email protected]",
"[email protected]",
},
"approvers": []any{
"[email protected]",
"[email protected]",
},
"changesRequestedBy": []any{
"[email protected]",
"[email protected]",
},
"contributors": []any{
"[email protected]",
"[email protected]",
},
"createdTime": float64(time.Date(
2023, time.April, 5, 1, 0, 0, 0, time.UTC).Unix()),
"docNumber": "ABC-123",
"docType": "RFC",
"fileRevisions": map[string]any{
"2": "FileRevision2",
"1": "FileRevision1",
},
"modifiedTime": float64(time.Date(
2023, time.April, 5, 23, 0, 0, 0, time.UTC).Unix()),
"owners": []any{"[email protected]"},
"product": "Product1",
"stakeholders": []any{
"[email protected]",
"[email protected]",
},
},
dbDoc: models.Document{
DocumentNumber: 123,
DocumentType: models.DocumentType{
Name: "RFC",
},
Product: models.Product{
Name: "Product1",
Abbreviation: "ABC",
},
Approvers: []*models.User{
{
EmailAddress: "[email protected]",
},
{
EmailAddress: "[email protected]",
},
},
Contributors: []*models.User{
{
EmailAddress: "[email protected]",
},
{
EmailAddress: "[email protected]",
},
},
CustomFields: []*models.DocumentCustomField{
{
DocumentTypeCustomField: models.DocumentTypeCustomField{
Name: "Stakeholders",
DocumentType: models.DocumentType{
Name: "RFC",
},
},
Value: `["[email protected]","[email protected]"]`,
},
},
DocumentCreatedAt: time.Date(
2023, time.April, 5, 1, 0, 0, 0, time.UTC),
DocumentModifiedAt: time.Date(
2023, time.April, 5, 23, 0, 0, 0, time.UTC),
FileRevisions: []models.DocumentFileRevision{
{
GoogleDriveFileRevisionID: "1",
Name: "FileRevision1",
},
{
GoogleDriveFileRevisionID: "2",
Name: "FileRevision2",
},
},
Owner: &models.User{
EmailAddress: "[email protected]",
},
},
dbDocReviews: models.DocumentReviews{
{
Status: models.ApprovedDocumentReviewStatus,
User: models.User{
EmailAddress: "[email protected]",
},
},
{
Status: models.ApprovedDocumentReviewStatus,
User: models.User{
EmailAddress: "[email protected]",
},
},
{
Status: models.ChangesRequestedDocumentReviewStatus,
User: models.User{
EmailAddress: "[email protected]",
},
},
{
Status: models.ChangesRequestedDocumentReviewStatus,
User: models.User{
EmailAddress: "[email protected]",
},
},
},
},

"bad objectID": {
algoDoc: map[string]any{
"objectID": "GoogleFileID1",
Expand Down Expand Up @@ -839,7 +971,7 @@ func TestCompareAlgoliaAndDatabaseDocument(t *testing.T) {
Owner: &models.User{
EmailAddress: "[email protected]",
},
Summary: "BadSummary1",
Summary: &[]string{"BadSummary1"}[0],
},
shouldErr: true,
errContains: "summary not equal",
Expand Down
Loading