diff --git a/.gitignore b/.gitignore index 7f47482..d4acff5 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ *.iml *.iws -/build \ No newline at end of file +/build +vendor/ diff --git a/README.md b/README.md index be078b5..7dc8a4d 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,9 @@ If on go 1.12 then ensure the project either resides outside of your `GOPATH` or | HealthCheckInterval | Interval between health checks | 30 seconds | | HealthCheckCriticalTimeout | Amount of time to pass since last healthy health check to be deemed a critical failure | 90 seconds | | RendererURL | URl for `dp-frontend-renderer` +| CacheUpdateInterval | Duration for homepage cache updation +| IsPublishingMode | Mode in which service is running +| Languages | Languages which are supported separated by comma ### Contributing diff --git a/config/config.go b/config/config.go index d239ad8..bbcaff9 100644 --- a/config/config.go +++ b/config/config.go @@ -15,6 +15,9 @@ type Config struct { HealthCheckCriticalTimeout time.Duration `envconfig:"HEALTHCHECK_CRITICAL_TIMEOUT"` RendererURL string `envconfig:"RENDERER_URL"` BabbageURL string `envconfig:"BABBAGE_URL"` + CacheUpdateInterval time.Duration `envconfig:"CACHE_UPDATE_INTERVAL"` + IsPublishingMode bool `envconfig:"IS_PUBLISHING_MODE"` + Languages string `envconfig:"LANGUAGES"` } var cfg *Config @@ -34,6 +37,9 @@ func Get() (*Config, error) { HealthCheckCriticalTimeout: 90 * time.Second, RendererURL: "http://localhost:20010", BabbageURL: "http://localhost:8080", + CacheUpdateInterval: 10 * time.Second, + IsPublishingMode: false, + Languages: "en,cy", } return cfg, envconfig.Process("", cfg) diff --git a/config/config_test.go b/config/config_test.go index 1c3a5c4..aa4353c 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -24,7 +24,9 @@ func TestSpec(t *testing.T) { So(cfg.HealthCheckInterval, ShouldEqual, 30*time.Second) So(cfg.HealthCheckCriticalTimeout, ShouldEqual, 90*time.Second) So(cfg.RendererURL, ShouldEqual, "http://localhost:20010") - So(cfg.BabbageURL, ShouldEqual, "http://localhost:8080") + So(cfg.IsPublishingMode, ShouldEqual, false) + So(cfg.CacheUpdateInterval, ShouldEqual, 10*time.Second) + So(cfg.Languages, ShouldEqual, "en,cy") }) }) }) diff --git a/dpcache/constants.go b/dpcache/constants.go new file mode 100644 index 0000000..dda2a50 --- /dev/null +++ b/dpcache/constants.go @@ -0,0 +1,6 @@ +package dpcache + +const ( + // HomepageCacheKey is used to cache the rendered homepage + HomepageCacheKey = "homepage-cache" +) diff --git a/dpcache/dpcache.go b/dpcache/dpcache.go new file mode 100644 index 0000000..00276f6 --- /dev/null +++ b/dpcache/dpcache.go @@ -0,0 +1,93 @@ +package dpcache + +import ( + "context" + "fmt" + "github.com/ONSdigital/log.go/log" + "sync" + "time" +) + +type DpCacher interface { + Close() + Get(key string) (interface{}, bool) + Set(key string, data interface{}) + AddUpdateFunc(key string, updateFunc func() (string, error)) + StartUpdates(ctx context.Context, channel chan error) +} + +type DpCache struct { + cache sync.Map + updateInterval time.Duration + close chan struct{} + updateFuncs map[string]func() (string, error) +} + +func (dc *DpCache) Get(key string) (interface{}, bool) { + return dc.cache.Load(key) +} + +func (dc *DpCache) Set(key string, data interface{}) { + dc.cache.Store(key, data) +} + +func (dc *DpCache) Close() { + dc.close <- struct{}{} + for key, _ := range dc.updateFuncs { + dc.cache.Store(key, "") + } + dc.updateFuncs = make(map[string]func() (string, error)) +} + +func NewDpCache(updateInterval time.Duration) DpCacher { + return &DpCache{ + cache: sync.Map{}, + updateInterval: updateInterval, + close: make(chan struct{}), + updateFuncs: make(map[string]func() (string, error)), + } +} + +func (dc *DpCache) AddUpdateFunc(key string, updateFunc func() (string, error)) { + dc.updateFuncs[key] = updateFunc +} + +func (dc *DpCache) UpdateContent(ctx context.Context) error { + for key, updateFunc := range dc.updateFuncs { + updatedContent, err := updateFunc() + if err != nil { + return fmt.Errorf("HOMEPAGE_CACHE_UPDATE_FAILED. failed to update homepage cache for %s. error: %v", key, err) + } + dc.Set(key, updatedContent) + } + return nil +} + +func (dc *DpCache) StartUpdates(ctx context.Context, errorChannel chan error) { + ticker := time.NewTicker(dc.updateInterval) + if len(dc.updateFuncs) == 0 { + return + } + + err := dc.UpdateContent(ctx) + if err != nil { + errorChannel <- err + dc.Close() + return + } + + for { + select { + case <-ticker.C: + err := dc.UpdateContent(ctx) + if err != nil { + log.Event(ctx, err.Error(), log.Error(err), log.ERROR) + } + + case <-dc.close: + return + case <-ctx.Done(): + return + } + } +} diff --git a/go.mod b/go.mod index 3e54593..6255ad5 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/ONSdigital/dp-healthcheck v1.0.5 github.com/ONSdigital/dp-net v1.0.11 github.com/ONSdigital/log.go v1.0.1 + github.com/ReneKroon/ttlcache v1.7.0 github.com/fatih/color v1.10.0 // indirect github.com/gorilla/mux v1.8.0 github.com/hokaccha/go-prettyjson v0.0.0-20210113012101-fb4e108d2519 // indirect diff --git a/go.sum b/go.sum index 8099e11..56b2e47 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,3 @@ -github.com/ONSdigital/dp-api-clients-go v1.28.0 h1:ExIUlHC6uBdBlFwt/gAI0ApSzpyigy0NWJFK3XCwSVc= github.com/ONSdigital/dp-api-clients-go v1.28.0/go.mod h1:iyJy6uRL4B6OYOJA0XMr5UHt6+Q8XmN9uwmURO+9Oj4= github.com/ONSdigital/dp-api-clients-go v1.33.0 h1:VVWJZSpmHOJvXUETwgAcFaizW6voxRU8RbsmPHma4GU= github.com/ONSdigital/dp-api-clients-go v1.33.0/go.mod h1:0pUK3MN1v7DTjq0JSAD+DqbsZ8AVTodrXSXgJecg9Pw= @@ -21,9 +20,13 @@ github.com/ONSdigital/log.go v1.0.1-0.20200805084515-ee61165ea36a/go.mod h1:dDnQ github.com/ONSdigital/log.go v1.0.1-0.20200805145532-1f25087a0744/go.mod h1:y4E9MYC+cV9VfjRD0UBGj8PA7H3wABqQi87/ejrDhYc= github.com/ONSdigital/log.go v1.0.1 h1:SZ5wRZAwlt2jQUZ9AUzBB/PL+iG15KapfQpJUdA18/4= github.com/ONSdigital/log.go v1.0.1/go.mod h1:dIwSXuvFB5EsZG5x44JhsXZKMd80zlb0DZxmiAtpL4M= +github.com/ReneKroon/ttlcache v1.7.0 h1:8BkjFfrzVFXyrqnMtezAaJ6AHPSsVV10m6w28N/Fgkk= +github.com/ReneKroon/ttlcache v1.7.0/go.mod h1:8BGGzdumrIjWxdRx8zpK6L3oGMWvIXdvB2GD1cfvd+I= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9 h1:wWke/RUCl7VRjQhwPlR/v0glZXNYzBHdNUzf/Am2Nmg= github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9/go.mod h1:uPmAp6Sws4L7+Q/OokbWDAK1ibXYhB3PXFP1kol5hPg= -github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= @@ -32,7 +35,6 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGa github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/hokaccha/go-prettyjson v0.0.0-20190818114111-108c894c2c0e h1:0aewS5NTyxftZHSnFaJmWE5oCCrj4DyEXkAiMa1iZJM= github.com/hokaccha/go-prettyjson v0.0.0-20190818114111-108c894c2c0e/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI= github.com/hokaccha/go-prettyjson v0.0.0-20210113012101-fb4e108d2519 h1:nqAlWFEdqI0ClbTDrhDvE/8LeQ4pftrqKUX9w5k0j3s= github.com/hokaccha/go-prettyjson v0.0.0-20210113012101-fb4e108d2519/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI= @@ -53,18 +55,24 @@ github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyex github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +go.uber.org/goleak v0.10.0 h1:G3eWbSNIskeRqtsN/1uI5B+eP73y3JUuBsv9AZjehb4= +go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= diff --git a/homepage/clients.go b/homepage/clients.go index ec6c45b..f8c30e1 100644 --- a/homepage/clients.go +++ b/homepage/clients.go @@ -2,31 +2,41 @@ package homepage import ( "context" - "github.com/ONSdigital/dp-api-clients-go/image" "github.com/ONSdigital/dp-api-clients-go/zebedee" "github.com/ONSdigital/dp-frontend-homepage-controller/clients/release_calendar" + health "github.com/ONSdigital/dp-healthcheck/healthcheck" ) -//go:generate moq -out mocks_test.go -pkg homepage . ZebedeeClient RenderClient BabbageClient ImageClient - // ZebedeeClient is an interface with methods required for a zebedee client type ZebedeeClient interface { GetTimeseriesMainFigure(ctx context.Context, userAuthToken, collectionID, lang, uri string) (m zebedee.TimeseriesMainFigure, err error) GetHomepageContent(ctx context.Context, userAccessToken, collectionID, lang, path string) (m zebedee.HomepageContent, err error) + Checker(ctx context.Context, check *health.CheckState) error } // BabbageClient is an interface with methods required for a babbage client type BabbageClient interface { GetReleaseCalendar(ctx context.Context, userAccessToken, fromDay, fromMonth, fromYear string) (m release_calendar.ReleaseCalendar, err error) + Checker(ctx context.Context, check *health.CheckState) error } // ImageClient is an interface with methods required for the Image API service client type ImageClient interface { GetDownloadVariant(ctx context.Context, userAuthToken, serviceAuthToken, collectionID, imageID, variant string) (m image.ImageDownload, err error) + Checker(ctx context.Context, check *health.CheckState) error } // RenderClient is an interface with methods required for rendering a template type RenderClient interface { Do(string, []byte) ([]byte, error) + Checker(ctx context.Context, check *health.CheckState) error +} + +// Clients contains all the required Clients for frontend homepage controller +type Clients struct { + Renderer RenderClient + Zebedee ZebedeeClient + Babbage BabbageClient + ImageAPI ImageClient } diff --git a/homepage/homepage.go b/homepage/homepage.go index e7662a5..a5dfeaf 100644 --- a/homepage/homepage.go +++ b/homepage/homepage.go @@ -2,15 +2,11 @@ package homepage import ( "context" - "encoding/json" "net/http" - "sync" "time" - "github.com/ONSdigital/dp-api-clients-go/image" "github.com/ONSdigital/dp-api-clients-go/zebedee" "github.com/ONSdigital/dp-frontend-homepage-controller/mapper" - model "github.com/ONSdigital/dp-frontend-models/model/homepage" dphandlers "github.com/ONSdigital/dp-net/handlers" "github.com/ONSdigital/log.go/log" ) @@ -34,100 +30,27 @@ type MainFigure struct { var mainFigureMap map[string]MainFigure // Handler handles requests to homepage endpoint -func Handler(rend RenderClient, zcli ZebedeeClient, bcli BabbageClient, icli ImageClient) http.HandlerFunc { +func Handler(homepageClient HomepageClienter) http.HandlerFunc { return dphandlers.ControllerHandler(func(w http.ResponseWriter, r *http.Request, lang, collectionID, accessToken string) { - handle(w, r, rend, zcli, bcli, icli, accessToken, collectionID, lang) + handle(w, r, accessToken, collectionID, lang, homepageClient) }) - } -func handle(w http.ResponseWriter, req *http.Request, rend RenderClient, zcli ZebedeeClient, bcli BabbageClient, icli ImageClient, userAccessToken, collectionID, lang string) { +func handle(w http.ResponseWriter, req *http.Request, userAccessToken, collectionID, lang string, homepageClient HomepageClienter) { ctx := req.Context() - - mappedMainFigures := make(map[string]*model.MainFigure) - var wg sync.WaitGroup - responses := make(chan *model.MainFigure, len(mainFigureMap)) - for id, figure := range mainFigureMap { - wg.Add(1) - go func(ctx context.Context, zcli ZebedeeClient, id string, figure MainFigure) { - defer wg.Done() - zebResponses := []zebedee.TimeseriesMainFigure{} - for _, uri := range figure.uris { - zebResponse, err := zcli.GetTimeseriesMainFigure(ctx, userAccessToken, collectionID, lang, uri) - if err != nil { - log.Event(ctx, "error getting timeseries data", log.ERROR, log.Error(err), log.Data{"timeseries-data": uri}) - mappedErrorFigure := &model.MainFigure{ID: id} - responses <- mappedErrorFigure - return - } - zebResponses = append(zebResponses, zebResponse) - } - trendInfo := getTrendInfo(ctx, userAccessToken, collectionID, lang, zcli, figure) - latestMainFigure := getLatestTimeSeriesData(ctx, zebResponses) - mappedMainFigure := mapper.MainFigure(ctx, id, figure.datePeriod, figure.differenceInterval, latestMainFigure, trendInfo) - responses <- mappedMainFigure - return - }(ctx, zcli, id, figure) - } - wg.Wait() - close(responses) - - for response := range responses { - log.Event(ctx, "the response of this request was", log.ERROR, log.Data{"response": response}) - mappedMainFigures[response.ID] = response - } - - weekAgoTime := time.Now().AddDate(0, 0, -7) - dateFromDay := weekAgoTime.Format("02") - dateFromMonth := weekAgoTime.Format("01") - dateFromYear := weekAgoTime.Format("2006") - releaseCalResp, err := bcli.GetReleaseCalendar(ctx, userAccessToken, dateFromDay, dateFromMonth, dateFromYear) + homepageHTML, err := homepageClient.GetHomePage(ctx, userAccessToken, collectionID, lang) if err != nil { - log.Event(ctx, "error failed to get release calendar data from babbage ", log.ERROR, log.Error(err)) - } - releaseCalModelData := mapper.ReleaseCalendar(releaseCalResp) - - // Get homepage data from Zebedee - homepageContent, err := zcli.GetHomepageContent(ctx, userAccessToken, collectionID, lang, HomepagePath) - if err != nil { - log.Event(ctx, "error getting homepage data from client", log.ERROR, log.Error(err), log.Data{"content-path": HomepagePath}) - } - - var mappedFeaturedContent []model.Feature - if len(homepageContent.FeaturedContent) > 0 { - imageObjects := map[string]image.ImageDownload{} - for _, fc := range homepageContent.FeaturedContent { - if fc.ImageID != "" { - image, err := icli.GetDownloadVariant(ctx, userAccessToken, "", "", fc.ImageID, ImageVariant) - if err != nil { - log.Event(ctx, "error getting image download variant", log.ERROR, log.Error(err), log.Data{"featured-content-entry": fc.Title}) - } - imageObjects[fc.ImageID] = image - } - } - mappedFeaturedContent = mapper.FeaturedContent(homepageContent, imageObjects) - } - - m := mapper.Homepage(lang, mappedMainFigures, releaseCalModelData, &mappedFeaturedContent, homepageContent.ServiceMessage) - - b, err := json.Marshal(m) - if err != nil { - log.Event(ctx, "error marshalling body data to json", log.ERROR, log.Error(err)) - http.Error(w, "error marshalling body data to json", http.StatusInternalServerError) + log.Event(ctx, "HOMEPAGE_RESPONSE_FAILED. failed to get homepage html", log.ERROR, log.Error(err)) + w.WriteHeader(http.StatusInternalServerError) return } - templateHTML, err := rend.Do("homepage", b) - if err != nil { - log.Event(ctx, "error rendering page", log.ERROR, log.Error(err)) - http.Error(w, "error rendering page", http.StatusInternalServerError) - return - } - if _, err := w.Write(templateHTML); err != nil { + if _, err := w.Write([]byte(homepageHTML)); err != nil { log.Event(ctx, "failed to write response for homepage", log.ERROR, log.Error(err)) w.WriteHeader(http.StatusInternalServerError) return } + return } diff --git a/homepage/homepage_publishing_client.go b/homepage/homepage_publishing_client.go new file mode 100644 index 0000000..ac372b6 --- /dev/null +++ b/homepage/homepage_publishing_client.go @@ -0,0 +1,31 @@ +package homepage + +import ( + "context" +) + +type HomepageClienter interface { + GetHomePage(ctx context.Context, userAccessToken, collectionID, lang string) (string, error) + Close() + StartBackgroundUpdate(ctx context.Context, errorChannel chan error) +} + +type HomepagePublishingClient struct { + HomepageUpdater +} + +func NewHomePagePublishingClient(clients *Clients) HomepageClienter { + return &HomepagePublishingClient{ + HomepageUpdater: HomepageUpdater{ + clients: clients, + }, + } +} + +func (hpc *HomepagePublishingClient) GetHomePage(ctx context.Context, userAccessToken, collectionID, lang string) (string, error) { + return hpc.GetHomePageUpdateFor(ctx, userAccessToken, collectionID, lang)() +} + +func (hpc *HomepagePublishingClient) Close() {} +func (hpc *HomepagePublishingClient) StartBackgroundUpdate(ctx context.Context, errorChannel chan error) { +} diff --git a/homepage/homepage_test.go b/homepage/homepage_test.go index fc0539f..439fc6c 100644 --- a/homepage/homepage_test.go +++ b/homepage/homepage_test.go @@ -2,6 +2,8 @@ package homepage import ( "context" + health "github.com/ONSdigital/dp-healthcheck/healthcheck" + "github.com/ReneKroon/ttlcache" "net/http" "net/http/httptest" "testing" @@ -189,41 +191,62 @@ func TestUnitMapper(t *testing.T) { GetHomepageContentFunc: func(ctx context.Context, userAuthToken, collectionID, lang, uri string) (m zebedee.HomepageContent, err error) { return mockedHomepageData, nil }, + CheckerFunc: func(ctx context.Context, check *health.CheckState) error { + return nil + }, } mockBabbageClient := &BabbageClientMock{ GetReleaseCalendarFunc: func(ctx context.Context, userAuthToken, fromDay, fromMonth, fromYear string) (m release_calendar.ReleaseCalendar, err error) { return mockedBabbageRelease, nil }, + CheckerFunc: func(ctx context.Context, check *health.CheckState) error { + return nil + }, } mockImageClient := &ImageClientMock{ GetDownloadVariantFunc: func(ctx context.Context, userAuthToken, serviceAuthToken, collectionID, imageID, variant string) (image.ImageDownload, error) { return mockedImageDownloadData[imageID], nil }, + CheckerFunc: func(ctx context.Context, check *health.CheckState) error { + return nil + }, } mockRenderClient := &RenderClientMock{ DoFunc: func(string, []byte) ([]byte, error) { return []byte(expectedSuccessResponse), nil }, + CheckerFunc: func(ctx context.Context, check *health.CheckState) error { + return nil + }, } - req := httptest.NewRequest("GET", "/", nil) - req.Header.Set("X-Florence-Token", "testuser") - rec := httptest.NewRecorder() - router := mux.NewRouter() - router.Path("/").HandlerFunc(Handler(mockRenderClient, mockZebedeeClient, mockBabbageClient, mockImageClient)) + req := httptest.NewRequest("GET", "/", nil) + req.Header.Set("X-Florence-Token", "testuser") + rec := httptest.NewRecorder() + router := mux.NewRouter() + cache := ttlcache.NewCache() + cache.SetTTL(10 * time.Millisecond) + clients := &Clients{ + Renderer: mockRenderClient, + Zebedee: mockZebedeeClient, + Babbage: mockBabbageClient, + ImageAPI: mockImageClient, + } + homepageClient := NewHomePagePublishingClient(clients) + router.Path("/").HandlerFunc(Handler(homepageClient)) - Convey("returns 200 response", func() { - router.ServeHTTP(rec, req) - So(rec.Code, ShouldEqual, http.StatusOK) - }) + Convey("returns 200 response", func() { + router.ServeHTTP(rec, req) + So(rec.Code, ShouldEqual, http.StatusOK) + }) - Convey("renderer returns HTML body", func() { - router.ServeHTTP(rec, req) - response := rec.Body.String() - So(response, ShouldEqual, expectedSuccessResponse) - }) + Convey("renderer returns HTML body", func() { + router.ServeHTTP(rec, req) + response := rec.Body.String() + So(response, ShouldEqual, expectedSuccessResponse) }) +}) } diff --git a/homepage/homepage_updater.go b/homepage/homepage_updater.go new file mode 100644 index 0000000..9b6a49b --- /dev/null +++ b/homepage/homepage_updater.go @@ -0,0 +1,103 @@ +package homepage + +import ( + "context" + "encoding/json" + "fmt" + "github.com/ONSdigital/dp-api-clients-go/image" + "github.com/ONSdigital/dp-api-clients-go/zebedee" + "github.com/ONSdigital/dp-frontend-homepage-controller/mapper" + model "github.com/ONSdigital/dp-frontend-models/model/homepage" + "github.com/ONSdigital/log.go/log" + "sync" + "time" +) + +type HomepageUpdater struct { + clients *Clients +} + +func (hu *HomepageUpdater) GetHomePageUpdateFor(ctx context.Context, userAccessToken, collectionID, lang string) func() (string, error) { + return func() (string, error) { + mappedMainFigures := make(map[string]*model.MainFigure) + var wg sync.WaitGroup + responses := make(chan *model.MainFigure, len(mainFigureMap)) + for id, figure := range mainFigureMap { + wg.Add(1) + go func(ctx context.Context, zcli ZebedeeClient, id string, figure MainFigure) { + defer wg.Done() + zebResponses := []zebedee.TimeseriesMainFigure{} + for _, uri := range figure.uris { + zebResponse, err := zcli.GetTimeseriesMainFigure(ctx, userAccessToken, collectionID, lang, uri) + if err != nil { + log.Event(ctx, "error getting timeseries data", log.ERROR, log.Error(err), log.Data{"timeseries-data": uri}) + mappedErrorFigure := &model.MainFigure{ID: id} + responses <- mappedErrorFigure + return + } + zebResponses = append(zebResponses, zebResponse) + } + trendInfo := getTrendInfo(ctx, userAccessToken, collectionID, lang, zcli, figure) + latestMainFigure := getLatestTimeSeriesData(ctx, zebResponses) + mappedMainFigure := mapper.MainFigure(ctx, id, figure.datePeriod, figure.differenceInterval, latestMainFigure, trendInfo) + responses <- mappedMainFigure + return + }(ctx, hu.clients.Zebedee, id, figure) + } + wg.Wait() + close(responses) + + for response := range responses { + log.Event(ctx, "the response of this request was", log.ERROR, log.Data{"response": response}) + mappedMainFigures[response.ID] = response + } + + weekAgoTime := time.Now().AddDate(0, 0, -7) + dateFromDay := weekAgoTime.Format("02") + dateFromMonth := weekAgoTime.Format("01") + dateFromYear := weekAgoTime.Format("2006") + releaseCalResp, err := hu.clients.Babbage.GetReleaseCalendar(ctx, userAccessToken, dateFromDay, dateFromMonth, dateFromYear) + if err != nil { + log.Event(ctx, "error failed to get release calendar data from babbage ", log.ERROR, log.Error(err)) + } + releaseCalModelData := mapper.ReleaseCalendar(releaseCalResp) + + // Get homepage data from Zebedee + homepageContent, err := hu.clients.Zebedee.GetHomepageContent(ctx, userAccessToken, collectionID, lang, HomepagePath) + if err != nil { + log.Event(ctx, "error getting homepage data from client", log.ERROR, log.Error(err), log.Data{"content-path": HomepagePath}) + } + + var mappedFeaturedContent []model.Feature + if len(homepageContent.FeaturedContent) > 0 { + imageObjects := map[string]image.ImageDownload{} + for _, fc := range homepageContent.FeaturedContent { + if fc.ImageID != "" { + image, err := hu.clients.ImageAPI.GetDownloadVariant(ctx, userAccessToken, "", "", fc.ImageID, ImageVariant) + if err != nil { + log.Event(ctx, "error getting image download variant", log.ERROR, log.Error(err), log.Data{"featured-content-entry": fc.Title}) + } + imageObjects[fc.ImageID] = image + } + } + mappedFeaturedContent = mapper.FeaturedContent(homepageContent, imageObjects) + } + + m := mapper.Homepage(lang, mappedMainFigures, releaseCalModelData, &mappedFeaturedContent, homepageContent.ServiceMessage) + + b, err := json.Marshal(m) + if err != nil { + errMessage := "error marshalling body data to json" + return "", fmt.Errorf("%s. error: %#v", errMessage, err) + } + + templateHTML, err := hu.clients.Renderer.Do("homepage", b) + if err != nil { + errMessage := "error rendering page" + return "", fmt.Errorf("%s. error: %#v", errMessage, err) + } + + return string(templateHTML), nil + } +} + diff --git a/homepage/homepage_web_client.go b/homepage/homepage_web_client.go new file mode 100644 index 0000000..f73de7a --- /dev/null +++ b/homepage/homepage_web_client.go @@ -0,0 +1,50 @@ +package homepage + +import ( + "context" + "fmt" + "github.com/ONSdigital/dp-frontend-homepage-controller/dpcache" + "time" +) + +type HomepageWebClient struct { + HomepageUpdater + cache dpcache.DpCacher + languages []string +} + +func NewHomePageWebClient(clients *Clients, updateInterval time.Duration, languages []string) HomepageClienter { + return &HomepageWebClient{ + HomepageUpdater: HomepageUpdater{ + clients: clients, + }, + cache: dpcache.NewDpCache(updateInterval), + languages: languages, + } +} + +func (hwc *HomepageWebClient) GetHomePage(ctx context.Context, userAccessToken, collectionID, lang string) (string, error) { + homePageCachedHTML, ok := hwc.cache.Get(getCachingKeyForLanguage(lang)) + if ok { + return fmt.Sprintf("%s", homePageCachedHTML), nil + } + + return "", fmt.Errorf("failed to read homepage from cache for: %s", getCachingKeyForLanguage(lang)) + +} + +func getCachingKeyForLanguage(lang string) string { + return fmt.Sprintf("%s___%s", dpcache.HomepageCacheKey, lang) +} + +func (hwc *HomepageWebClient) StartBackgroundUpdate(ctx context.Context, errorChannel chan error) { + for _, lang := range hwc.languages { + langKey := getCachingKeyForLanguage(lang) + hwc.cache.AddUpdateFunc(langKey, hwc.GetHomePageUpdateFor(ctx, "", "", lang)) + } + + hwc.cache.StartUpdates(ctx, errorChannel) +} +func (hwc *HomepageWebClient) Close() { + hwc.cache.Close() +} diff --git a/homepage/mocks_test.go b/homepage/mocks_test.go index 7d0639f..1640089 100644 --- a/homepage/mocks_test.go +++ b/homepage/mocks_test.go @@ -8,6 +8,7 @@ import ( "github.com/ONSdigital/dp-api-clients-go/image" "github.com/ONSdigital/dp-api-clients-go/zebedee" "github.com/ONSdigital/dp-frontend-homepage-controller/clients/release_calendar" + health "github.com/ONSdigital/dp-healthcheck/healthcheck" "sync" ) @@ -17,23 +18,29 @@ var _ ZebedeeClient = &ZebedeeClientMock{} // ZebedeeClientMock is a mock implementation of ZebedeeClient. // -// func TestSomethingThatUsesZebedeeClient(t *testing.T) { +// func TestSomethingThatUsesZebedeeClient(t *testing.T) { // -// // make and configure a mocked ZebedeeClient -// mockedZebedeeClient := &ZebedeeClientMock{ -// GetHomepageContentFunc: func(ctx context.Context, userAccessToken string, collectionID string, lang string, path string) (zebedee.HomepageContent, error) { -// panic("mock out the GetHomepageContent method") -// }, -// GetTimeseriesMainFigureFunc: func(ctx context.Context, userAuthToken string, collectionID string, lang string, uri string) (zebedee.TimeseriesMainFigure, error) { -// panic("mock out the GetTimeseriesMainFigure method") -// }, -// } +// // make and configure a mocked ZebedeeClient +// mockedZebedeeClient := &ZebedeeClientMock{ +// CheckerFunc: func(ctx context.Context, check *health.CheckState) error { +// panic("mock out the Checker method") +// }, +// GetHomepageContentFunc: func(ctx context.Context, userAccessToken string, collectionID string, lang string, path string) (zebedee.HomepageContent, error) { +// panic("mock out the GetHomepageContent method") +// }, +// GetTimeseriesMainFigureFunc: func(ctx context.Context, userAuthToken string, collectionID string, lang string, uri string) (zebedee.TimeseriesMainFigure, error) { +// panic("mock out the GetTimeseriesMainFigure method") +// }, +// } // -// // use mockedZebedeeClient in code that requires ZebedeeClient -// // and then make assertions. +// // use mockedZebedeeClient in code that requires ZebedeeClient +// // and then make assertions. // -// } +// } type ZebedeeClientMock struct { + // CheckerFunc mocks the Checker method. + CheckerFunc func(ctx context.Context, check *health.CheckState) error + // GetHomepageContentFunc mocks the GetHomepageContent method. GetHomepageContentFunc func(ctx context.Context, userAccessToken string, collectionID string, lang string, path string) (zebedee.HomepageContent, error) @@ -42,6 +49,13 @@ type ZebedeeClientMock struct { // calls tracks calls to the methods. calls struct { + // Checker holds details about calls to the Checker method. + Checker []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Check is the check argument value. + Check *health.CheckState + } // GetHomepageContent holds details about calls to the GetHomepageContent method. GetHomepageContent []struct { // Ctx is the ctx argument value. @@ -69,10 +83,46 @@ type ZebedeeClientMock struct { URI string } } + lockChecker sync.RWMutex lockGetHomepageContent sync.RWMutex lockGetTimeseriesMainFigure sync.RWMutex } +// Checker calls CheckerFunc. +func (mock *ZebedeeClientMock) Checker(ctx context.Context, check *health.CheckState) error { + if mock.CheckerFunc == nil { + panic("ZebedeeClientMock.CheckerFunc: method is nil but ZebedeeClient.Checker was just called") + } + callInfo := struct { + Ctx context.Context + Check *health.CheckState + }{ + Ctx: ctx, + Check: check, + } + mock.lockChecker.Lock() + mock.calls.Checker = append(mock.calls.Checker, callInfo) + mock.lockChecker.Unlock() + return mock.CheckerFunc(ctx, check) +} + +// CheckerCalls gets all the calls that were made to Checker. +// Check the length with: +// len(mockedZebedeeClient.CheckerCalls()) +func (mock *ZebedeeClientMock) CheckerCalls() []struct { + Ctx context.Context + Check *health.CheckState +} { + var calls []struct { + Ctx context.Context + Check *health.CheckState + } + mock.lockChecker.RLock() + calls = mock.calls.Checker + mock.lockChecker.RUnlock() + return calls +} + // GetHomepageContent calls GetHomepageContentFunc. func (mock *ZebedeeClientMock) GetHomepageContent(ctx context.Context, userAccessToken string, collectionID string, lang string, path string) (zebedee.HomepageContent, error) { if mock.GetHomepageContentFunc == nil { @@ -173,64 +223,113 @@ var _ RenderClient = &RenderClientMock{} // RenderClientMock is a mock implementation of RenderClient. // -// func TestSomethingThatUsesRenderClient(t *testing.T) { +// func TestSomethingThatUsesRenderClient(t *testing.T) { // -// // make and configure a mocked RenderClient -// mockedRenderClient := &RenderClientMock{ -// DoFunc: func(in1 string, in2 []byte) ([]byte, error) { -// panic("mock out the Do method") -// }, -// } +// // make and configure a mocked RenderClient +// mockedRenderClient := &RenderClientMock{ +// CheckerFunc: func(ctx context.Context, check *health.CheckState) error { +// panic("mock out the Checker method") +// }, +// DoFunc: func(s string, bytes []byte) ([]byte, error) { +// panic("mock out the Do method") +// }, +// } // -// // use mockedRenderClient in code that requires RenderClient -// // and then make assertions. +// // use mockedRenderClient in code that requires RenderClient +// // and then make assertions. // -// } +// } type RenderClientMock struct { + // CheckerFunc mocks the Checker method. + CheckerFunc func(ctx context.Context, check *health.CheckState) error + // DoFunc mocks the Do method. - DoFunc func(in1 string, in2 []byte) ([]byte, error) + DoFunc func(s string, bytes []byte) ([]byte, error) // calls tracks calls to the methods. calls struct { + // Checker holds details about calls to the Checker method. + Checker []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Check is the check argument value. + Check *health.CheckState + } // Do holds details about calls to the Do method. Do []struct { - // In1 is the in1 argument value. - In1 string - // In2 is the in2 argument value. - In2 []byte + // S is the s argument value. + S string + // Bytes is the bytes argument value. + Bytes []byte } } - lockDo sync.RWMutex + lockChecker sync.RWMutex + lockDo sync.RWMutex +} + +// Checker calls CheckerFunc. +func (mock *RenderClientMock) Checker(ctx context.Context, check *health.CheckState) error { + if mock.CheckerFunc == nil { + panic("RenderClientMock.CheckerFunc: method is nil but RenderClient.Checker was just called") + } + callInfo := struct { + Ctx context.Context + Check *health.CheckState + }{ + Ctx: ctx, + Check: check, + } + mock.lockChecker.Lock() + mock.calls.Checker = append(mock.calls.Checker, callInfo) + mock.lockChecker.Unlock() + return mock.CheckerFunc(ctx, check) +} + +// CheckerCalls gets all the calls that were made to Checker. +// Check the length with: +// len(mockedRenderClient.CheckerCalls()) +func (mock *RenderClientMock) CheckerCalls() []struct { + Ctx context.Context + Check *health.CheckState +} { + var calls []struct { + Ctx context.Context + Check *health.CheckState + } + mock.lockChecker.RLock() + calls = mock.calls.Checker + mock.lockChecker.RUnlock() + return calls } // Do calls DoFunc. -func (mock *RenderClientMock) Do(in1 string, in2 []byte) ([]byte, error) { +func (mock *RenderClientMock) Do(s string, bytes []byte) ([]byte, error) { if mock.DoFunc == nil { panic("RenderClientMock.DoFunc: method is nil but RenderClient.Do was just called") } callInfo := struct { - In1 string - In2 []byte + S string + Bytes []byte }{ - In1: in1, - In2: in2, + S: s, + Bytes: bytes, } mock.lockDo.Lock() mock.calls.Do = append(mock.calls.Do, callInfo) mock.lockDo.Unlock() - return mock.DoFunc(in1, in2) + return mock.DoFunc(s, bytes) } // DoCalls gets all the calls that were made to Do. // Check the length with: // len(mockedRenderClient.DoCalls()) func (mock *RenderClientMock) DoCalls() []struct { - In1 string - In2 []byte + S string + Bytes []byte } { var calls []struct { - In1 string - In2 []byte + S string + Bytes []byte } mock.lockDo.RLock() calls = mock.calls.Do @@ -238,31 +337,45 @@ func (mock *RenderClientMock) DoCalls() []struct { return calls } + // Ensure, that BabbageClientMock does implement BabbageClient. // If this is not the case, regenerate this file with moq. var _ BabbageClient = &BabbageClientMock{} // BabbageClientMock is a mock implementation of BabbageClient. // -// func TestSomethingThatUsesBabbageClient(t *testing.T) { +// func TestSomethingThatUsesBabbageClient(t *testing.T) { // -// // make and configure a mocked BabbageClient -// mockedBabbageClient := &BabbageClientMock{ -// GetReleaseCalendarFunc: func(ctx context.Context, userAccessToken string, fromDay string, fromMonth string, fromYear string) (release_calendar.ReleaseCalendar, error) { -// panic("mock out the GetReleaseCalendar method") -// }, -// } +// // make and configure a mocked BabbageClient +// mockedBabbageClient := &BabbageClientMock{ +// CheckerFunc: func(ctx context.Context, check *health.CheckState) error { +// panic("mock out the Checker method") +// }, +// GetReleaseCalendarFunc: func(ctx context.Context, userAccessToken string, fromDay string, fromMonth string, fromYear string) (release_calendar.ReleaseCalendar, error) { +// panic("mock out the GetReleaseCalendar method") +// }, +// } // -// // use mockedBabbageClient in code that requires BabbageClient -// // and then make assertions. +// // use mockedBabbageClient in code that requires BabbageClient +// // and then make assertions. // -// } +// } type BabbageClientMock struct { + // CheckerFunc mocks the Checker method. + CheckerFunc func(ctx context.Context, check *health.CheckState) error + // GetReleaseCalendarFunc mocks the GetReleaseCalendar method. GetReleaseCalendarFunc func(ctx context.Context, userAccessToken string, fromDay string, fromMonth string, fromYear string) (release_calendar.ReleaseCalendar, error) // calls tracks calls to the methods. calls struct { + // Checker holds details about calls to the Checker method. + Checker []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Check is the check argument value. + Check *health.CheckState + } // GetReleaseCalendar holds details about calls to the GetReleaseCalendar method. GetReleaseCalendar []struct { // Ctx is the ctx argument value. @@ -277,9 +390,45 @@ type BabbageClientMock struct { FromYear string } } + lockChecker sync.RWMutex lockGetReleaseCalendar sync.RWMutex } +// Checker calls CheckerFunc. +func (mock *BabbageClientMock) Checker(ctx context.Context, check *health.CheckState) error { + if mock.CheckerFunc == nil { + panic("BabbageClientMock.CheckerFunc: method is nil but BabbageClient.Checker was just called") + } + callInfo := struct { + Ctx context.Context + Check *health.CheckState + }{ + Ctx: ctx, + Check: check, + } + mock.lockChecker.Lock() + mock.calls.Checker = append(mock.calls.Checker, callInfo) + mock.lockChecker.Unlock() + return mock.CheckerFunc(ctx, check) +} + +// CheckerCalls gets all the calls that were made to Checker. +// Check the length with: +// len(mockedBabbageClient.CheckerCalls()) +func (mock *BabbageClientMock) CheckerCalls() []struct { + Ctx context.Context + Check *health.CheckState +} { + var calls []struct { + Ctx context.Context + Check *health.CheckState + } + mock.lockChecker.RLock() + calls = mock.calls.Checker + mock.lockChecker.RUnlock() + return calls +} + // GetReleaseCalendar calls GetReleaseCalendarFunc. func (mock *BabbageClientMock) GetReleaseCalendar(ctx context.Context, userAccessToken string, fromDay string, fromMonth string, fromYear string) (release_calendar.ReleaseCalendar, error) { if mock.GetReleaseCalendarFunc == nil { @@ -333,25 +482,38 @@ var _ ImageClient = &ImageClientMock{} // ImageClientMock is a mock implementation of ImageClient. // -// func TestSomethingThatUsesImageClient(t *testing.T) { +// func TestSomethingThatUsesImageClient(t *testing.T) { // -// // make and configure a mocked ImageClient -// mockedImageClient := &ImageClientMock{ -// GetDownloadVariantFunc: func(ctx context.Context, userAuthToken string, serviceAuthToken string, collectionID string, imageID string, variant string) (image.ImageDownload, error) { -// panic("mock out the GetDownloadVariant method") -// }, -// } +// // make and configure a mocked ImageClient +// mockedImageClient := &ImageClientMock{ +// CheckerFunc: func(ctx context.Context, check *health.CheckState) error { +// panic("mock out the Checker method") +// }, +// GetDownloadVariantFunc: func(ctx context.Context, userAuthToken string, serviceAuthToken string, collectionID string, imageID string, variant string) (image.ImageDownload, error) { +// panic("mock out the GetDownloadVariant method") +// }, +// } // -// // use mockedImageClient in code that requires ImageClient -// // and then make assertions. +// // use mockedImageClient in code that requires ImageClient +// // and then make assertions. // -// } +// } type ImageClientMock struct { + // CheckerFunc mocks the Checker method. + CheckerFunc func(ctx context.Context, check *health.CheckState) error + // GetDownloadVariantFunc mocks the GetDownloadVariant method. GetDownloadVariantFunc func(ctx context.Context, userAuthToken string, serviceAuthToken string, collectionID string, imageID string, variant string) (image.ImageDownload, error) // calls tracks calls to the methods. calls struct { + // Checker holds details about calls to the Checker method. + Checker []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Check is the check argument value. + Check *health.CheckState + } // GetDownloadVariant holds details about calls to the GetDownloadVariant method. GetDownloadVariant []struct { // Ctx is the ctx argument value. @@ -368,9 +530,45 @@ type ImageClientMock struct { Variant string } } + lockChecker sync.RWMutex lockGetDownloadVariant sync.RWMutex } +// Checker calls CheckerFunc. +func (mock *ImageClientMock) Checker(ctx context.Context, check *health.CheckState) error { + if mock.CheckerFunc == nil { + panic("ImageClientMock.CheckerFunc: method is nil but ImageClient.Checker was just called") + } + callInfo := struct { + Ctx context.Context + Check *health.CheckState + }{ + Ctx: ctx, + Check: check, + } + mock.lockChecker.Lock() + mock.calls.Checker = append(mock.calls.Checker, callInfo) + mock.lockChecker.Unlock() + return mock.CheckerFunc(ctx, check) +} + +// CheckerCalls gets all the calls that were made to Checker. +// Check the length with: +// len(mockedImageClient.CheckerCalls()) +func (mock *ImageClientMock) CheckerCalls() []struct { + Ctx context.Context + Check *health.CheckState +} { + var calls []struct { + Ctx context.Context + Check *health.CheckState + } + mock.lockChecker.RLock() + calls = mock.calls.Checker + mock.lockChecker.RUnlock() + return calls +} + // GetDownloadVariant calls GetDownloadVariantFunc. func (mock *ImageClientMock) GetDownloadVariant(ctx context.Context, userAuthToken string, serviceAuthToken string, collectionID string, imageID string, variant string) (image.ImageDownload, error) { if mock.GetDownloadVariantFunc == nil { @@ -420,4 +618,4 @@ func (mock *ImageClientMock) GetDownloadVariantCalls() []struct { calls = mock.calls.GetDownloadVariant mock.lockGetDownloadVariant.RUnlock() return calls -} +} \ No newline at end of file diff --git a/routes/routes.go b/routes/routes.go index b62c13b..316a531 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -4,28 +4,15 @@ import ( "context" "net/http" - "github.com/ONSdigital/dp-frontend-homepage-controller/clients/release_calendar" - - "github.com/ONSdigital/dp-api-clients-go/image" - "github.com/ONSdigital/dp-api-clients-go/renderer" - "github.com/ONSdigital/dp-api-clients-go/zebedee" "github.com/ONSdigital/dp-frontend-homepage-controller/homepage" "github.com/ONSdigital/log.go/log" "github.com/gorilla/mux" ) -// Clients contains all the required Clients for frontend homepage controller -type Clients struct { - Renderer *renderer.Renderer - Zebedee *zebedee.Client - Babbage *release_calendar.Client - ImageAPI *image.Client -} - // Init initialises routes for the service -func Init(ctx context.Context, r *mux.Router, hcHandler func(http.ResponseWriter, *http.Request), c *Clients) { +func Init(ctx context.Context, r *mux.Router, hcHandler func(http.ResponseWriter, *http.Request), homepageClient homepage.HomepageClienter) { log.Event(ctx, "adding routes", log.INFO) r.StrictSlash(true).Path("/health").HandlerFunc(hcHandler) - r.StrictSlash(true).Path("/").Methods("GET").HandlerFunc(homepage.Handler(c.Renderer, c.Zebedee, c.Babbage, c.ImageAPI)) + r.StrictSlash(true).Path("/").Methods("GET").HandlerFunc(homepage.Handler(homepageClient)) } diff --git a/service/service.go b/service/service.go index d3e3a85..c47ad2b 100644 --- a/service/service.go +++ b/service/service.go @@ -2,17 +2,18 @@ package service import ( "context" - "github.com/ONSdigital/dp-api-clients-go/health" "github.com/ONSdigital/dp-api-clients-go/image" "github.com/ONSdigital/dp-api-clients-go/renderer" "github.com/ONSdigital/dp-api-clients-go/zebedee" "github.com/ONSdigital/dp-frontend-homepage-controller/clients/release_calendar" "github.com/ONSdigital/dp-frontend-homepage-controller/config" + "github.com/ONSdigital/dp-frontend-homepage-controller/homepage" "github.com/ONSdigital/dp-frontend-homepage-controller/routes" "github.com/ONSdigital/log.go/log" "github.com/gorilla/mux" "github.com/pkg/errors" + "strings" ) // Service contains all the configs, server and clients to run the frontend homepage controller @@ -21,8 +22,9 @@ type Service struct { routerHealthClient *health.Client HealthCheck HealthChecker Server HTTPServer - clients *routes.Clients + clients *homepage.Clients ServiceList *ExternalServiceList + HomePageClient homepage.HomepageClienter } // Run the service @@ -39,7 +41,7 @@ func Run(ctx context.Context, cfg *config.Config, serviceList *ExternalServiceLi svc.routerHealthClient = serviceList.GetHealthClient("api-router", cfg.APIRouterURL) // Initialise clients - svc.clients = &routes.Clients{ + svc.clients = &homepage.Clients{ Renderer: renderer.New(cfg.RendererURL), Zebedee: zebedee.NewWithHealthClient(svc.routerHealthClient), Babbage: release_calendar.New(cfg.BabbageURL), @@ -56,9 +58,17 @@ func Run(ctx context.Context, cfg *config.Config, serviceList *ExternalServiceLi return nil, errors.Wrap(err, "unable to register checkers") } + if cfg.IsPublishingMode { + svc.HomePageClient = homepage.NewHomePagePublishingClient(svc.clients) + } else { + languages := strings.Split(cfg.Languages, ",") + svc.HomePageClient = homepage.NewHomePageWebClient(svc.clients, cfg.CacheUpdateInterval, languages) + go svc.HomePageClient.StartBackgroundUpdate(ctx, svcErrors) + } + // Initialise router r := mux.NewRouter() - routes.Init(ctx, r, svc.HealthCheck.Handler, svc.clients) + routes.Init(ctx, r, svc.HealthCheck.Handler, svc.HomePageClient) svc.Server = serviceList.GetHTTPServer(cfg.BindAddr, r) // Start Healthcheck and HTTP Server @@ -88,6 +98,10 @@ func (svc *Service) Close(ctx context.Context) error { svc.HealthCheck.Stop() } + if svc.HomePageClient != nil { + svc.HomePageClient.Close() + } + // stop any incoming requests if err := svc.Server.Shutdown(ctx); err != nil { log.Event(ctx, "failed to shutdown http server", log.Error(err), log.ERROR) diff --git a/service/service_test.go b/service/service_test.go index c75d827..8a7da35 100644 --- a/service/service_test.go +++ b/service/service_test.go @@ -43,6 +43,7 @@ func TestRun(t *testing.T) { Convey("Having a set of mocked dependencies", t, func() { cfg, err := config.Get() + cfg.IsPublishingMode = true So(err, ShouldBeNil) hcMock := &mock.HealthCheckerMock{