You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@beam.apache.org by da...@apache.org on 2022/09/08 15:36:19 UTC

[beam] branch master updated: [Tour Of Beam][backend] integration tests and GA workflow (#23032)

This is an automated email from the ASF dual-hosted git repository.

damccorm pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git


The following commit(s) were added to refs/heads/master by this push:
     new 9efa3787aef [Tour Of Beam][backend] integration tests and GA workflow (#23032)
9efa3787aef is described below

commit 9efa3787aefe9198c7985dd30b16691cdba61a7e
Author: Evgeny Antyshev <ea...@gmail.com>
AuthorDate: Thu Sep 8 18:36:10 2022 +0300

    [Tour Of Beam][backend] integration tests and GA workflow (#23032)
    
    * tests
    
    * WF
    
    * chdir
    
    * order
    
    * nit
    
    * fix
    
    * nit
    
    * concurrency
    
    * integration_go
    
    * tags
    
    * cache_go
    
    * nit
    
    * nit
    
    * rat
    
    * datadir
    
    * env
    
    * env
    
    * reduce fun
    
    * removing unicode character
    
    * Update learning/tour-of-beam/backend/integration_tests/client.go
    
    Co-authored-by: Danny McCormick <da...@google.com>
    
    * Update learning/tour-of-beam/backend/integration_tests/function_test.go
    
    Co-authored-by: Danny McCormick <da...@google.com>
    
    * review
    
    * nit
    
    Co-authored-by: oborysevych <ol...@akvelon.com>
    Co-authored-by: Danny McCormick <da...@google.com>
---
 .github/workflows/tour_of_beam_backend.yml         |   8 +-
 .../workflows/tour_of_beam_backend_integration.yml |  96 +++++++++++++
 build.gradle.kts                                   |   3 +
 learning/tour-of-beam/backend/docker-compose.yml   |   4 +-
 learning/tour-of-beam/backend/function.go          |   1 +
 .../tour-of-beam/backend/integration_tests/api.go  |  64 +++++++++
 .../backend/integration_tests/client.go            |  67 ++++++++++
 .../backend/integration_tests/function_test.go     | 148 +++++++++++++++++++++
 .../backend/integration_tests/local.sh             |  55 ++++++++
 .../backend/internal/fs_content/load_test.go       |   6 +-
 .../backend/internal/storage/image/Dockerfile      |   2 -
 .../backend/internal/storage/image/index.yaml      |  29 ----
 .../backend/internal/storage/index.yaml            |  20 +++
 .../backend/samples/api/get_unit_content.json      |  18 +--
 .../backend/samples/api/get_unit_content_full.json |  15 +++
 .../java/module 1/unit-challenge/description.md    |   2 +-
 .../java/module 1/unit-challenge/hint1.md          |   2 +-
 .../java/module 1/unit-challenge/hint2.md          |   2 +-
 .../java/module 2/unit-challenge/description.md    |   2 +-
 .../java/module 2/unit-challenge/hint1.md          |   2 +-
 .../java/module 2/unit-challenge/hint2.md          |   2 +-
 .../module 1/group/unit-challenge/description.md   |   2 +-
 .../python/module 1/group/unit-challenge/hint1.md  |   2 +-
 .../python/module 1/group/unit-challenge/hint2.md  |   2 +-
 24 files changed, 497 insertions(+), 57 deletions(-)

diff --git a/.github/workflows/tour_of_beam_backend.yml b/.github/workflows/tour_of_beam_backend.yml
index 2c186c710c3..7182911b3c2 100644
--- a/.github/workflows/tour_of_beam_backend.yml
+++ b/.github/workflows/tour_of_beam_backend.yml
@@ -17,7 +17,7 @@
 
 # To learn more about GitHub Actions in Apache Beam check the CI.md
 
-name: Tour of Beam Go tests
+name: Tour of Beam Go unittests
 
 on:
   push:
@@ -28,6 +28,11 @@ on:
     tags: 'v*'
     paths: ['learning/tour-of-beam/backend/**']
 
+# This allows a subsequently queued workflow run to interrupt previous runs
+concurrency:
+  group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}'
+  cancel-in-progress: true
+
 jobs:
   checks:
     runs-on: ubuntu-latest
@@ -38,6 +43,7 @@ jobs:
       - uses: actions/checkout@v3
       - uses: actions/setup-go@v3
         with:
+          # pin to the biggest Go version supported by Cloud Functions runtime
           go-version: '1.16'
       - name: Run fmt
         run: |
diff --git a/.github/workflows/tour_of_beam_backend_integration.yml b/.github/workflows/tour_of_beam_backend_integration.yml
new file mode 100644
index 00000000000..f584c7f7e00
--- /dev/null
+++ b/.github/workflows/tour_of_beam_backend_integration.yml
@@ -0,0 +1,96 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# To learn more about GitHub Actions in Apache Beam check the CI.md
+
+name: Tour of Beam Go integration tests
+
+on:
+  push:
+    branches: ['master', 'release-*']
+    tags: 'v*'
+  pull_request:
+    branches: ['master', 'release-*']
+    tags: 'v*'
+    paths: ['learning/tour-of-beam/backend/**']
+
+# This allows a subsequently queued workflow run to interrupt previous runs
+concurrency:
+  group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}'
+  cancel-in-progress: true
+
+env:
+  TOB_LEARNING_ROOT: ./samples/learning-content
+  DATASTORE_PROJECT_ID: test-proj
+  DATASTORE_EMULATOR_HOST: localhost:8081
+  DATASTORE_EMULATOR_DATADIR: ./datadir
+  PORT_SDK_LIST: 8801
+  PORT_GET_CONTENT_TREE: 8802
+  PORT_GET_UNIT_CONTENT: 8803
+
+
+jobs:
+  integration:
+    runs-on: ubuntu-latest
+    defaults:
+      run:
+        working-directory: ./learning/tour-of-beam/backend
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-go@v3
+        with:
+          # pin to the biggest Go version supported by Cloud Functions runtime
+          go-version: '1.16'
+
+      # 1. Datastore emulator
+      - name: 'Set up Cloud SDK'
+        uses: 'google-github-actions/setup-gcloud@v0'
+        with:
+          version: 397.0.0
+          project_id: ${{ env.DATASTORE_PROJECT_ID }}
+          install_components: 'beta,cloud-datastore-emulator'
+      - name: 'Start datastore emulator'
+        run: |
+          gcloud beta emulators datastore start \
+          --data-dir=${{ env.DATASTORE_EMULATOR_DATADIR }} \
+          --host-port=${{ env.DATASTORE_EMULATOR_HOST }} \
+          --consistency=1 &
+
+      # 2. start function-framework processes in BG
+      - name: Compile CF
+        run: go build -o ./tob_function cmd/main.go
+      - name: Run sdkList in background
+        run: PORT=${{ env.PORT_SDK_LIST }} FUNCTION_TARGET=sdkList ./tob_function &
+      - name: Run getContentTree in background
+        run: PORT=${{ env.PORT_GET_CONTENT_TREE }} FUNCTION_TARGET=getContentTree ./tob_function &
+      - name: Run getUnitContent in background
+        run: PORT=${{ env.PORT_GET_UNIT_CONTENT }} FUNCTION_TARGET=getUnitContent ./tob_function &
+
+      # 3. Load data in datastore: run CD step on samples/learning-content
+      - name: Run CI/CD to populate datastore
+        run: go run cmd/ci_cd/ci_cd.go
+
+      # 4. Check sdkList, getContentTree, getUnitContent: run integration tests
+      - name: Go integration tests
+        run: go test -v --tags integration ./integration_tests/...
+      # 5. Compare storage/datastore/index.yml VS generated
+      - name: Check index.yaml
+        run: |
+          diff -q "${{ env.DATASTORE_EMULATOR_DATADIR }}/WEB-INF/index.yaml" \
+          internal/storage/index.yaml \
+          || ( echo "index.yaml mismatch"; exit 1)
+
diff --git a/build.gradle.kts b/build.gradle.kts
index 88ded595c5b..7e6a8c9118a 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -115,6 +115,9 @@ tasks.rat {
     "learning/tour-of-beam/**/unit-info.yaml",
     "learning/tour-of-beam/backend/samples/**/*.md",
 
+    // Tour Of Beam backend autogenerated Datastore indexes
+    "learning/tour-of-beam/backend/internal/storage/index.yaml",
+
 
     // test p8 file for SnowflakeIO
     "sdks/java/io/snowflake/src/test/resources/invalid_test_rsa_key.p8",
diff --git a/learning/tour-of-beam/backend/docker-compose.yml b/learning/tour-of-beam/backend/docker-compose.yml
index b5ebcf08eaa..5205903791a 100644
--- a/learning/tour-of-beam/backend/docker-compose.yml
+++ b/learning/tour-of-beam/backend/docker-compose.yml
@@ -18,9 +18,11 @@ version: "3"
 services:
   datastore:
     build: internal/storage/image
+    volumes:
+      - ${DATASTORE_EMULATOR_DATADIR}:/opt/data
     environment:
       - DATASTORE_PROJECT_ID=project-test
       - DATASTORE_LISTEN_ADDRESS=0.0.0.0:8081
     ports:
       - "8081:8081"
-    command: --no-store-on-disk --consistency=1.0
+    command: --consistency=1.0
diff --git a/learning/tour-of-beam/backend/function.go b/learning/tour-of-beam/backend/function.go
index 6c6e34bd311..126c00b5cbd 100644
--- a/learning/tour-of-beam/backend/function.go
+++ b/learning/tour-of-beam/backend/function.go
@@ -107,6 +107,7 @@ func init() {
 	if os.Getenv("TOB_MOCK") > "" {
 		svc = &service.Mock{}
 	} else {
+		// consumes DATASTORE_* env variables
 		client, err := datastore.NewClient(context.Background(), "")
 		if err != nil {
 			log.Fatalf("new datastore client: %v", err)
diff --git a/learning/tour-of-beam/backend/integration_tests/api.go b/learning/tour-of-beam/backend/integration_tests/api.go
new file mode 100644
index 00000000000..f8bb8b38aef
--- /dev/null
+++ b/learning/tour-of-beam/backend/integration_tests/api.go
@@ -0,0 +1,64 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+// Reduced version of API, for integration tests and documentation
+// * No hidden fields
+// * Internal enumerations: sdk, node.type to string params
+
+type sdkListResponse struct {
+	Names []string
+}
+
+type Unit struct {
+	Id   string `json:"unitId"`
+	Name string `json:"name"`
+
+	// optional
+	Description       string   `json:"description,omitempty"`
+	Hints             []string `json:"hints,omitempty"`
+	TaskSnippetId     string   `json:"taskSnippetId,omitempty"`
+	SolutionSnippetId string   `json:"solutionSnippetId,omitempty"`
+
+	// optional, user-specific
+	UserSnippetId string `json:"userSnippetId,omitempty"`
+	IsCompleted   bool   `json:"isCompleted,omitempty"`
+}
+
+type Group struct {
+	Name  string `json:"name"`
+	Nodes []Node `json:"nodes"`
+}
+
+type Node struct {
+	Type  string `json:"type"`
+	Group *Group `json:"group,omitempty"`
+	Unit  *Unit  `json:"unit,omitempty"`
+}
+
+type Module struct {
+	Id         string `json:"moduleId"`
+	Name       string `json:"name"`
+	Complexity string `json:"complexity"`
+	Nodes      []Node `json:"nodes"`
+}
+
+type ContentTree struct {
+	Sdk     string   `json:"sdk"`
+	Modules []Module `json:"modules"`
+}
+
+type ErrorResponse struct {
+	Code    string `json:"code"`
+	Message string `json:"message,omitempty"`
+}
diff --git a/learning/tour-of-beam/backend/integration_tests/client.go b/learning/tour-of-beam/backend/integration_tests/client.go
new file mode 100644
index 00000000000..c66ab779527
--- /dev/null
+++ b/learning/tour-of-beam/backend/integration_tests/client.go
@@ -0,0 +1,67 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+	"encoding/json"
+	"io"
+	"net/http"
+	"os"
+)
+
+func SdkList(url string) (sdkListResponse, error) {
+	var result sdkListResponse
+	err := Get(&result, url, nil)
+	return result, err
+}
+
+func GetContentTree(url, sdk string) (ContentTree, error) {
+	var result ContentTree
+	err := Get(&result, url, map[string]string{"sdk": sdk})
+	return result, err
+}
+
+func GetUnitContent(url, sdk, unitId string) (Unit, error) {
+	var result Unit
+	err := Get(&result, url, map[string]string{"sdk": sdk, "unitId": unitId})
+	return result, err
+}
+
+// Generic HTTP call wrapper
+// params:
+// * dst: response struct pointer
+// * url: request  url
+// * query_params: url query params, as a map (we don't use multiple-valued params)
+func Get(dst interface{}, url string, queryParams map[string]string) error {
+	req, err := http.NewRequest(http.MethodGet, url, nil)
+	if err != nil {
+		return err
+	}
+	req.Header.Add("Content-Type", "application/json")
+	if len(queryParams) > 0 {
+		q := req.URL.Query()
+		for k, v := range queryParams {
+			q.Add(k, v)
+		}
+		req.URL.RawQuery = q.Encode()
+	}
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		return err
+	}
+
+	defer resp.Body.Close()
+
+	tee := io.TeeReader(resp.Body, os.Stdout)
+	return json.NewDecoder(tee).Decode(dst)
+}
diff --git a/learning/tour-of-beam/backend/integration_tests/function_test.go b/learning/tour-of-beam/backend/integration_tests/function_test.go
new file mode 100644
index 00000000000..92ced75f5e7
--- /dev/null
+++ b/learning/tour-of-beam/backend/integration_tests/function_test.go
@@ -0,0 +1,148 @@
+//go:build integration
+// +build integration
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+	"encoding/json"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+const (
+	PORT_SDK_LIST         = "PORT_SDK_LIST"
+	PORT_GET_CONTENT_TREE = "PORT_GET_CONTENT_TREE"
+	PORT_GET_UNIT_CONTENT = "PORT_GET_UNIT_CONTENT"
+)
+
+// scenarios:
+// + Get SDK list
+// + Get content tree for existing SDK
+// + Get content tree for non-existing SDK: 404 Not Found
+// + Get unit content for existing SDK, existing unitId
+// + Get unit content for non-existing SDK/unitId: 404 Not Found
+// TODO:
+// - Get content tree for a registered user
+// - Get unit content for a registered user
+// - Save user code/progress for a registered user
+// - (negative) Save user code/progress w/o user token/bad token
+// - (negative) Save user code/progress for non-existing SDK/unitId: 404 Not Found
+
+func loadJson(path string, dst interface{}) error {
+	fh, err := os.Open(path)
+	if err != nil {
+		return err
+	}
+	return json.NewDecoder(fh).Decode(dst)
+}
+
+func TestSdkList(t *testing.T) {
+	port := os.Getenv(PORT_SDK_LIST)
+	if port == "" {
+		t.Fatal(PORT_SDK_LIST, "env not set")
+	}
+	url := "http://localhost:" + port
+	exp := sdkListResponse{
+		Names: []string{"Java", "Python", "Go"},
+	}
+
+	resp, err := SdkList(url)
+	if err != nil {
+		t.Fatal(err)
+	}
+	assert.Equal(t, exp, resp)
+}
+
+func TestGetContentTree(t *testing.T) {
+	port := os.Getenv(PORT_GET_CONTENT_TREE)
+	if port == "" {
+		t.Fatal(PORT_GET_CONTENT_TREE, "env not set")
+	}
+	url := "http://localhost:" + port
+
+	mock_path := filepath.Join("..", "samples", "api", "get_content_tree.json")
+	var exp ContentTree
+	if err := loadJson(mock_path, &exp); err != nil {
+		t.Fatal(err)
+	}
+
+	resp, err := GetContentTree(url, "Python")
+	if err != nil {
+		t.Fatal(err)
+	}
+	assert.Equal(t, exp, resp)
+}
+
+func TestGetUnitContent(t *testing.T) {
+	port := os.Getenv(PORT_GET_UNIT_CONTENT)
+	if port == "" {
+		t.Fatal(PORT_GET_UNIT_CONTENT, "env not set")
+	}
+	url := "http://localhost:" + port
+
+	mock_path := filepath.Join("..", "samples", "api", "get_unit_content.json")
+	var exp Unit
+	if err := loadJson(mock_path, &exp); err != nil {
+		t.Fatal(err)
+	}
+
+	resp, err := GetUnitContent(url, "Python", "challenge1")
+	if err != nil {
+		t.Fatal(err)
+	}
+	assert.Equal(t, exp, resp)
+}
+
+func TestNegative(t *testing.T) {
+	for i, params := range []struct {
+		portEnvName string
+		queryParams map[string]string
+		expected    ErrorResponse
+	}{
+		{PORT_GET_CONTENT_TREE, nil,
+			ErrorResponse{
+				Code:    "BAD_FORMAT",
+				Message: "Sdk not in: [Java Python Go SCIO]",
+			},
+		},
+		{PORT_GET_CONTENT_TREE, map[string]string{"sdk": "SCIO"},
+			// TODO: actually here should be a NOT_FOUND error
+			ErrorResponse{Code: "INTERNAL_ERROR", Message: "storage error"},
+		},
+		{PORT_GET_UNIT_CONTENT, map[string]string{"sdk": "Python", "unitId": "unknown_unitId"},
+			ErrorResponse{
+				Code:    "NOT_FOUND",
+				Message: "unit not found",
+			},
+		},
+	} {
+		t.Log("Scenario", i)
+		port := os.Getenv(params.portEnvName)
+		if port == "" {
+			t.Fatal(params.portEnvName, "env not set")
+		}
+		url := "http://localhost:" + port
+
+		var resp ErrorResponse
+		err := Get(&resp, url, params.queryParams)
+		if err != nil {
+			t.Fatal(err)
+		}
+		assert.Equal(t, params.expected, resp)
+	}
+}
diff --git a/learning/tour-of-beam/backend/integration_tests/local.sh b/learning/tour-of-beam/backend/integration_tests/local.sh
new file mode 100644
index 00000000000..c19c3fb97c8
--- /dev/null
+++ b/learning/tour-of-beam/backend/integration_tests/local.sh
@@ -0,0 +1,55 @@
+#!/bin/bash -eux
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# 
+#     http://www.apache.org/licenses/LICENSE-2.0
+# 
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+export DATASTORE_PROJECT_ID=test-proj
+export DATASTORE_EMULATOR_HOST=localhost:8081
+export DATASTORE_EMULATOR_DATADIR=./datadir-$(date '+%H-%M-%S')
+export TOB_LEARNING_ROOT=./samples/learning-content
+
+export PORT_SDK_LIST=8801
+export PORT_GET_CONTENT_TREE=8802
+export PORT_GET_UNIT_CONTENT=8803
+
+mkdir "$DATASTORE_EMULATOR_DATADIR"
+
+docker-compose up -d
+
+go build -o tob_function cmd/main.go
+
+PORT=$PORT_SDK_LIST FUNCTION_TARGET=sdkList         ./tob_function &
+PORT=$PORT_GET_CONTENT_TREE FUNCTION_TARGET=getContentTree  ./tob_function &
+PORT=$PORT_GET_UNIT_CONTENT FUNCTION_TARGET=getUnitContent  ./tob_function &
+
+sleep 5
+
+
+go run cmd/ci_cd/ci_cd.go
+
+
+go test -v --tags integration ./integration_tests/...
+
+pkill -P $$
+
+rm -f ./tob_function
+
+docker-compose down
+
+
+ls "$DATASTORE_EMULATOR_DATADIR"
+cat "$DATASTORE_EMULATOR_DATADIR/WEB-INF/index.yaml"
+
+diff -q "$DATASTORE_EMULATOR_DATADIR/WEB-INF/index.yaml" internal/storage/index.yaml || ( echo "index.yaml mismatch"; exit 1)
+
+
+rm -rf "$DATASTORE_EMULATOR_DATADIR"
diff --git a/learning/tour-of-beam/backend/internal/fs_content/load_test.go b/learning/tour-of-beam/backend/internal/fs_content/load_test.go
index def3e9c1cd6..b5c50424dc7 100644
--- a/learning/tour-of-beam/backend/internal/fs_content/load_test.go
+++ b/learning/tour-of-beam/backend/internal/fs_content/load_test.go
@@ -26,10 +26,10 @@ import (
 func genUnitNode(id string) tob.Node {
 	return tob.Node{Type: tob.NODE_UNIT, Unit: &tob.Unit{
 		Id: id, Name: "Challenge Name",
-		Description: "## Challenge description\n\nbla bla bla",
+		Description: "## Challenge description\n\nawesome description\n",
 		Hints: []string{
-			"## Hint 1\n\napply yourself :)",
-			"## Hint 2\n\napply more",
+			"## Hint 1\n\nhint 1",
+			"## Hint 2\n\nhint 2",
 		},
 	}}
 }
diff --git a/learning/tour-of-beam/backend/internal/storage/image/Dockerfile b/learning/tour-of-beam/backend/internal/storage/image/Dockerfile
index 071e7e37c92..0ff411bf352 100644
--- a/learning/tour-of-beam/backend/internal/storage/image/Dockerfile
+++ b/learning/tour-of-beam/backend/internal/storage/image/Dockerfile
@@ -23,8 +23,6 @@ FROM google/cloud-sdk:$GCLOUD_SDK_VERSION
 # Volume to persist Datastore data
 VOLUME /opt/data
 
-# RUN mkdir -p /opt/data/WEB-INF
-COPY index.yaml /opt/data/WEB-INF/index.yaml
 COPY start-datastore.sh .
 
 EXPOSE 8081
diff --git a/learning/tour-of-beam/backend/internal/storage/image/index.yaml b/learning/tour-of-beam/backend/internal/storage/image/index.yaml
deleted file mode 100644
index c59bc238758..00000000000
--- a/learning/tour-of-beam/backend/internal/storage/image/index.yaml
+++ /dev/null
@@ -1,29 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements.  See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License.  You may obtain a copy of the License at
-#
-#    http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-indexes:
-- kind: tb_learning_module
-  ancestor: yes
-  properties:
-    - name: order
-
-- kind: tb_learning_node
-  ancestor: yes
-  properties:
-  - name: level
-  - name: order
-  - name: id
-  - name: name
-  - name: type
\ No newline at end of file
diff --git a/learning/tour-of-beam/backend/internal/storage/index.yaml b/learning/tour-of-beam/backend/internal/storage/index.yaml
new file mode 100644
index 00000000000..fa78d72f948
--- /dev/null
+++ b/learning/tour-of-beam/backend/internal/storage/index.yaml
@@ -0,0 +1,20 @@
+indexes:
+# AUTOGENERATED
+
+# This index.yaml is automatically updated whenever the Cloud Datastore
+# emulator detects that a new type of query is run. If you want to manage the
+# index.yaml file manually, remove the "# AUTOGENERATED" marker line above.
+# If you want to manage some indexes manually, move them above the marker line.
+
+- kind: "tb_learning_module"
+  ancestor: yes
+  properties:
+  - name: "order"
+- kind: "tb_learning_node"
+  ancestor: yes
+  properties:
+  - name: "level"
+  - name: "order"
+  - name: "id"
+  - name: "name"
+  - name: "type"
diff --git a/learning/tour-of-beam/backend/samples/api/get_unit_content.json b/learning/tour-of-beam/backend/samples/api/get_unit_content.json
index 0774f09116f..82337277ff0 100644
--- a/learning/tour-of-beam/backend/samples/api/get_unit_content.json
+++ b/learning/tour-of-beam/backend/samples/api/get_unit_content.json
@@ -1,15 +1,9 @@
 {
-    "unitId": "1.1",
-    "name": "Basic concepts",
-    "description": "Lorem ipsum...",
+    "unitId": "challenge1",
+    "name": "Challenge Name",
+    "description": "## Challenge description\n\nawesome description\n",
     "hints" : [
-        "## Hint 1\n\napply yourself :)",
-        "## Hint 2\n\napply more"
-     ],
-
-    "taskSnippetId": "taskSnippetId",
-    "solutionSnippetId": "solutionSnippetId",
-
-    "userSnippetId": "userSnippetId",
-    "isCompleted": true
+        "## Hint 1\n\nhint 1",
+        "## Hint 2\n\nhint 2"
+     ]
 }
diff --git a/learning/tour-of-beam/backend/samples/api/get_unit_content_full.json b/learning/tour-of-beam/backend/samples/api/get_unit_content_full.json
new file mode 100644
index 00000000000..3fc1bc26e53
--- /dev/null
+++ b/learning/tour-of-beam/backend/samples/api/get_unit_content_full.json
@@ -0,0 +1,15 @@
+{
+    "unitId": "challenge1",
+    "name": "Challenge Name",
+    "description": "## Challenge description\n\nawesome description\n",
+    "hints" : [
+        "## Hint 1\n\nhint 1",
+        "## Hint 2\n\nhint 2"
+     ],
+
+    "taskSnippetId": "taskSnippetId",
+    "solutionSnippetId": "solutionSnippetId",
+
+    "userSnippetId": "userSnippetId",
+    "isCompleted": true
+}
diff --git a/learning/tour-of-beam/backend/samples/learning-content/java/module 1/unit-challenge/description.md b/learning/tour-of-beam/backend/samples/learning-content/java/module 1/unit-challenge/description.md
index 1906064ed20..ee159851f94 100644
--- a/learning/tour-of-beam/backend/samples/learning-content/java/module 1/unit-challenge/description.md	
+++ b/learning/tour-of-beam/backend/samples/learning-content/java/module 1/unit-challenge/description.md	
@@ -1,3 +1,3 @@
 ## Challenge description
 
-bla bla bla
\ No newline at end of file
+awesome description
diff --git a/learning/tour-of-beam/backend/samples/learning-content/java/module 1/unit-challenge/hint1.md b/learning/tour-of-beam/backend/samples/learning-content/java/module 1/unit-challenge/hint1.md
index e5d1a67fa90..0d023e307f5 100644
--- a/learning/tour-of-beam/backend/samples/learning-content/java/module 1/unit-challenge/hint1.md	
+++ b/learning/tour-of-beam/backend/samples/learning-content/java/module 1/unit-challenge/hint1.md	
@@ -1,3 +1,3 @@
 ## Hint 1
 
-apply yourself :)
\ No newline at end of file
+hint 1
\ No newline at end of file
diff --git a/learning/tour-of-beam/backend/samples/learning-content/java/module 1/unit-challenge/hint2.md b/learning/tour-of-beam/backend/samples/learning-content/java/module 1/unit-challenge/hint2.md
index 0c425751be9..92b0bf0af2f 100644
--- a/learning/tour-of-beam/backend/samples/learning-content/java/module 1/unit-challenge/hint2.md	
+++ b/learning/tour-of-beam/backend/samples/learning-content/java/module 1/unit-challenge/hint2.md	
@@ -1,3 +1,3 @@
 ## Hint 2
 
-apply more
\ No newline at end of file
+hint 2
\ No newline at end of file
diff --git a/learning/tour-of-beam/backend/samples/learning-content/java/module 2/unit-challenge/description.md b/learning/tour-of-beam/backend/samples/learning-content/java/module 2/unit-challenge/description.md
index 1906064ed20..ee159851f94 100644
--- a/learning/tour-of-beam/backend/samples/learning-content/java/module 2/unit-challenge/description.md	
+++ b/learning/tour-of-beam/backend/samples/learning-content/java/module 2/unit-challenge/description.md	
@@ -1,3 +1,3 @@
 ## Challenge description
 
-bla bla bla
\ No newline at end of file
+awesome description
diff --git a/learning/tour-of-beam/backend/samples/learning-content/java/module 2/unit-challenge/hint1.md b/learning/tour-of-beam/backend/samples/learning-content/java/module 2/unit-challenge/hint1.md
index e5d1a67fa90..0d023e307f5 100644
--- a/learning/tour-of-beam/backend/samples/learning-content/java/module 2/unit-challenge/hint1.md	
+++ b/learning/tour-of-beam/backend/samples/learning-content/java/module 2/unit-challenge/hint1.md	
@@ -1,3 +1,3 @@
 ## Hint 1
 
-apply yourself :)
\ No newline at end of file
+hint 1
\ No newline at end of file
diff --git a/learning/tour-of-beam/backend/samples/learning-content/java/module 2/unit-challenge/hint2.md b/learning/tour-of-beam/backend/samples/learning-content/java/module 2/unit-challenge/hint2.md
index 0c425751be9..92b0bf0af2f 100644
--- a/learning/tour-of-beam/backend/samples/learning-content/java/module 2/unit-challenge/hint2.md	
+++ b/learning/tour-of-beam/backend/samples/learning-content/java/module 2/unit-challenge/hint2.md	
@@ -1,3 +1,3 @@
 ## Hint 2
 
-apply more
\ No newline at end of file
+hint 2
\ No newline at end of file
diff --git a/learning/tour-of-beam/backend/samples/learning-content/python/module 1/group/unit-challenge/description.md b/learning/tour-of-beam/backend/samples/learning-content/python/module 1/group/unit-challenge/description.md
index 1906064ed20..ee159851f94 100644
--- a/learning/tour-of-beam/backend/samples/learning-content/python/module 1/group/unit-challenge/description.md	
+++ b/learning/tour-of-beam/backend/samples/learning-content/python/module 1/group/unit-challenge/description.md	
@@ -1,3 +1,3 @@
 ## Challenge description
 
-bla bla bla
\ No newline at end of file
+awesome description
diff --git a/learning/tour-of-beam/backend/samples/learning-content/python/module 1/group/unit-challenge/hint1.md b/learning/tour-of-beam/backend/samples/learning-content/python/module 1/group/unit-challenge/hint1.md
index e5d1a67fa90..0d023e307f5 100644
--- a/learning/tour-of-beam/backend/samples/learning-content/python/module 1/group/unit-challenge/hint1.md	
+++ b/learning/tour-of-beam/backend/samples/learning-content/python/module 1/group/unit-challenge/hint1.md	
@@ -1,3 +1,3 @@
 ## Hint 1
 
-apply yourself :)
\ No newline at end of file
+hint 1
\ No newline at end of file
diff --git a/learning/tour-of-beam/backend/samples/learning-content/python/module 1/group/unit-challenge/hint2.md b/learning/tour-of-beam/backend/samples/learning-content/python/module 1/group/unit-challenge/hint2.md
index 0c425751be9..92b0bf0af2f 100644
--- a/learning/tour-of-beam/backend/samples/learning-content/python/module 1/group/unit-challenge/hint2.md	
+++ b/learning/tour-of-beam/backend/samples/learning-content/python/module 1/group/unit-challenge/hint2.md	
@@ -1,3 +1,3 @@
 ## Hint 2
 
-apply more
\ No newline at end of file
+hint 2
\ No newline at end of file