diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 96bb740..e7fc3b4 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -13,10 +13,10 @@ jobs: uses: actions/checkout@v4 # step 2: set up go - - name: Set up Go 1.22 - uses: actions/setup-go@v4 + - name: Set up Go + uses: actions/setup-go@v5 with: - go-version: "1.22" + go-version: stable # step 3: install dependencies - name: Install all Go dependencies @@ -26,10 +26,35 @@ jobs: - name: Run coverage run: go test -race -coverprofile=coverage.out -covermode=atomic ./... + e2e: + name: E2E + runs-on: ubuntu-latest + steps: + # step 1: checkout repository code + - name: Checkout code into workspace directory + uses: actions/checkout@v4 + + # step 2: set up go + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: stable + + # step 3: start services + - name: Start services + env: + "FCM__CREDENTIALS_JSON": ${{ secrets.FCM__CREDENTIALS_JSON }} + run: docker compose -f test/e2e/docker-compose.yml up -d --build + + # step 4: run test + - name: Run e2e tests + run: cd test/e2e && go test . + build: name: Build needs: - test + - e2e uses: ./.github/workflows/docker-build.yml with: app-name: sms-gateway diff --git a/Makefile b/Makefile index 907f517..4bf70d6 100644 --- a/Makefile +++ b/Makefile @@ -33,6 +33,7 @@ lint: test: go test -race -coverprofile=coverage.out -covermode=atomic ./... + cd test/e2e && go test . build: go build -o tmp/$(project_name) ./cmd/$(project_name) diff --git a/test/e2e/data/10-init.sql b/test/e2e/data/10-init.sql new file mode 100644 index 0000000..e7dec59 --- /dev/null +++ b/test/e2e/data/10-init.sql @@ -0,0 +1,6 @@ +CREATE DATABASE `sms-public`; +CREATE DATABASE `sms-private`; +--- +CREATE USER 'sms' @'%' IDENTIFIED BY 'sms'; +GRANT ALL PRIVILEGES ON `sms-public`.* TO 'sms' @'%'; +GRANT ALL PRIVILEGES ON `sms-private`.* TO 'sms' @'%'; \ No newline at end of file diff --git a/test/e2e/data/config.yml b/test/e2e/data/config.yml new file mode 100644 index 0000000..2ac0627 --- /dev/null +++ b/test/e2e/data/config.yml @@ -0,0 +1,3 @@ +--- +fcm: + credentials_json: "{}" diff --git a/test/e2e/docker-compose.yml b/test/e2e/docker-compose.yml new file mode 100644 index 0000000..0044501 --- /dev/null +++ b/test/e2e/docker-compose.yml @@ -0,0 +1,71 @@ +services: + public: + image: android-sms-gateway/server + build: + context: ../.. + dockerfile: ./build/package/Dockerfile + args: + - APP=sms-gateway + environment: + - DEBUG= + - CONFIG_PATH=config.yml + - GOOSE_DBSTRING=sms:sms@tcp(db:3306)/sms + - HTTP__LISTEN=0.0.0.0:3000 + - DATABASE__HOST=db + - DATABASE__PORT=3306 + - DATABASE__USER=sms + - DATABASE__PASSWORD=sms + - DATABASE__DATABASE=sms-public + - GATEWAY__MODE=public + - FCM__CREDENTIALS_JSON=${FCM__CREDENTIALS_JSON} + ports: + - "3000:3000" + volumes: + - ./data/config.yml:/app/config.yml:ro + restart: 'unless-stopped' + depends_on: + db: + condition: service_healthy + + private: + image: android-sms-gateway/server + build: + context: ../.. + dockerfile: ./build/package/Dockerfile + args: + - APP=sms-gateway + environment: + - DEBUG= + - CONFIG_PATH=config.yml + - GOOSE_DBSTRING=sms:sms@tcp(db:3306)/sms + - HTTP__LISTEN=0.0.0.0:3000 + - DATABASE__HOST=db + - DATABASE__PORT=3306 + - DATABASE__USER=sms + - DATABASE__PASSWORD=sms + - DATABASE__DATABASE=sms-private + - GATEWAY__MODE=private + - GATEWAY__PRIVATE_TOKEN=123456789 + ports: + - "3001:3000" + volumes: + - ./data/config.yml:/app/config.yml:ro + restart: 'unless-stopped' + depends_on: + db: + condition: service_healthy + + db: + image: mariadb:lts + environment: + - MARIADB_ROOT_PASSWORD=root + - MARIADB_AUTO_UPGRADE=1 + volumes: + - ./data/10-init.sql:/docker-entrypoint-initdb.d/10-init.sql:ro + restart: 'unless-stopped' + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + start_period: 4s + interval: 4s + timeout: 2s + retries: 3 diff --git a/test/e2e/e2e.go b/test/e2e/e2e.go new file mode 100644 index 0000000..df8caf7 --- /dev/null +++ b/test/e2e/e2e.go @@ -0,0 +1 @@ +package e2e diff --git a/test/e2e/go.mod b/test/e2e/go.mod new file mode 100644 index 0000000..2561a64 --- /dev/null +++ b/test/e2e/go.mod @@ -0,0 +1,7 @@ +module github.com/android-sms-gateway/server/test/e2e + +go 1.22.0 + +require github.com/go-resty/resty/v2 v2.16.2 + +require golang.org/x/net v0.27.0 // indirect diff --git a/test/e2e/go.sum b/test/e2e/go.sum new file mode 100644 index 0000000..7aa1cc1 --- /dev/null +++ b/test/e2e/go.sum @@ -0,0 +1,6 @@ +github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg= +github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= diff --git a/test/e2e/main_test.go b/test/e2e/main_test.go new file mode 100644 index 0000000..176e03b --- /dev/null +++ b/test/e2e/main_test.go @@ -0,0 +1,68 @@ +package e2e + +import ( + "fmt" + "log" + "os" + "os/exec" + "testing" + "time" + + "github.com/go-resty/resty/v2" +) + +const ( + PublicURL = "http://localhost:3000/api" + PrivateURL = "http://localhost:3001/api" +) + +func isOnline() bool { + for _, v := range []string{PublicURL, PrivateURL} { + _, err := resty.New(). + SetBaseURL(v). + SetTimeout(100 * time.Millisecond). + R(). + Get("/health") + + if err != nil { + log.Println("waiting for health", err) + return false + } + } + + return true +} + +func TestMain(m *testing.M) { + log.Println("running e2e tests") + + if _, ok := os.LookupEnv("CI"); !ok { + if err := exec.Command("docker", "compose", "up", "-d", "--build").Run(); err != nil { + log.Fatal(fmt.Errorf("docker-compose up -d: %w", err)) + } + + defer func() { + if err := exec.Command("docker", "compose", "down", "-v").Run(); err != nil { + log.Fatal(fmt.Errorf("docker-compose down -v: %w", err)) + } + log.Println("e2e tests finished") + }() + } + + startedAt := time.Now() + for { + if time.Since(startedAt) > 20*time.Second { + log.Println("timeout") + return + } + + if isOnline() { + log.Println("e2e tests started") + break + } + + time.Sleep(1 * time.Second) + } + + m.Run() +} diff --git a/test/e2e/mobile_test.go b/test/e2e/mobile_test.go new file mode 100644 index 0000000..69d08a2 --- /dev/null +++ b/test/e2e/mobile_test.go @@ -0,0 +1,96 @@ +package e2e + +import ( + "testing" + "time" + + "github.com/go-resty/resty/v2" +) + +func makeClient(baseUrl string) *resty.Client { + return resty.New(). + SetBaseURL(baseUrl). + SetTimeout(300 * time.Millisecond) +} + +func TestPublicDeviceRegister(t *testing.T) { + cases := []struct { + headers map[string]string + expectedStatusCode int + }{ + { + headers: map[string]string{ + "Authorization": "Bearer 123456789", + }, + expectedStatusCode: 201, + }, + { + headers: map[string]string{}, + expectedStatusCode: 201, + }, + { + headers: map[string]string{ + "Authorization": "Bearer 987654321", + }, + expectedStatusCode: 201, + }, + } + + client := makeClient(PublicURL + "/mobile/v1/device") + + for _, c := range cases { + res, err := client.R(). + SetHeader("Content-Type", "application/json"). + SetBody(`{"name": "Public Device Name", "pushToken": "token"}`). + SetHeaders(c.headers). + Post("") + if err != nil { + t.Fatal(err) + } + + if res.StatusCode() != c.expectedStatusCode { + t.Fatal(res.StatusCode(), res.String()) + } + } +} + +func TestPrivateDeviceRegister(t *testing.T) { + cases := []struct { + headers map[string]string + expectedStatusCode int + }{ + { + headers: map[string]string{ + "Authorization": "Bearer 123456789", + }, + expectedStatusCode: 201, + }, + { + headers: map[string]string{}, + expectedStatusCode: 401, + }, + { + headers: map[string]string{ + "Authorization": "Bearer 987654321", + }, + expectedStatusCode: 401, + }, + } + + client := makeClient(PrivateURL + "/mobile/v1/device") + + for _, c := range cases { + res, err := client.R(). + SetHeader("Content-Type", "application/json"). + SetBody(`{"name": "Private Device Name", "pushToken": "token"}`). + SetHeaders(c.headers). + Post("") + if err != nil { + t.Fatal(err) + } + + if res.StatusCode() != c.expectedStatusCode { + t.Fatal(res.StatusCode(), res.String()) + } + } +}