Merge branch 'master' of gitlab.com:slon/shad-go-private

This commit is contained in:
Fedor Korotkiy 2020-02-20 22:15:50 +03:00
commit b70a688ccf
42 changed files with 840 additions and 0 deletions

View file

@ -12,3 +12,13 @@
score: 100
- task: fetchall
score: 100
- group: Basics
start: 20-02-2020 18:00
deadline: 02-03-2020 23:59
tasks:
- task: digitalclock
score: 300
- task: urlshortener
score: 200

114
digitalclock/README.md Normal file
View file

@ -0,0 +1,114 @@
## digitalclock
В этой задаче нужно написать http server,
который по запросу `/?time=hh:mm:ss` вернет PNG с данным временем.
Сервер должен слушать порт, переданный через аргумент `--port`.
### Примеры
Запуск:
```
$ digitalclock -port 6029
```
После запуска можно открыть в браузере `http://localhost:6029/?time=15:04:05` и получить png наподобие этой:
![Image description](assets/150405.png)
Если параметр `time` не задан (или равен ""), сервен должен вернуть текущее время.
В этом случае при обновлении страницы картинка должна меняться.
В запросе также нужно поддержать параметр 1 <=`k` <= 30,
который отвечает за увеличение картинки в `k` раз:
http://localhost:6029/?time=15:04:05?k=15
![Image description](assets/150405_15.png)
На невалидных `time` и `k` сервер должен возврщать произвольное сообщение об ошибке и http status 400 (Bad Request):
```
$ curl -i "http://localhost:6029/?k=100"
HTTP/1.1 400 Bad Request
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Fri, 14 Feb 2020 02:02:41 GMT
Content-Length: 10
invalid k
```
В случае успеха приложение должно возвращать http status 200 (OK),
а также проставлять http header `Content-Type: image/png`:
```
$ curl -i "http://localhost:6029/"
HTTP/1.1 200 OK
Content-Type: image/png
Date: Fri, 14 Feb 2020 02:08:19 GMT
Content-Length: 193
...
```
### Формат
`hh:mm:ss`
В [./symbols.go](./symbols.go) определены строковые константы, кодирующие цифры и двоеточие.
Один символ константы (отличный от '\n') - 1 пиксель.
'.' должны кодироваться в белый цвет, '1' - в бирюзовый (переменная **Cyan** в [./symbols.go](./symbols.go)).
Все константы имеют одинаковую высоту `h`, все цифры имеют одинаковую ширину `w`.
`k` отличный от 1 (default) превращает каждый пиксель `p` картинки в блок `k x k` пикселей `p`.
Высота результирующей картинки - `h * k`, ширина - `(6 * w + 2 * w_colon) * k`,
где `w_colon` ширина константы, соответствующей ':'.
### Проверка решения
Для запуска тестов нужно выполнить следующую команду:
```
go test -v ./digitalclock/...
```
### Запуск программы
```
go run -v ./digitalclock/...
```
### Компиляция
```
go install ./digitalclock/...
```
После выполнения в `$GOPATH/bin` появится исполняемый файл с именем `digitalclock`.
### Walkthrough
1. парсинг аргументов командной строки https://gobyexample.com/command-line-flags
2. http server: https://p.go.manytask.org/00-intro/lecture.slide#23
3. url params: https://golang.org/pkg/net/url/#URL.Query
3. форматирование времени: https://gobyexample.com/time-formatting-parsing
4. работа с картинками: https://golang.org/pkg/image/
Пример, создания простой png размера 5x5:
```go
package main
import (
"image"
"image/color"
"image/png"
"os"
)
func main() {
img := image.NewRGBA(image.Rect(0, 0, 5, 5))
img.Set(2, 2, color.RGBA{255, 0, 0, 255})
f, _ := os.Create("/tmp/img.png")
png.Encode(f, img)
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

7
digitalclock/main.go Normal file
View file

@ -0,0 +1,7 @@
// +build !solution
package main
func main() {
}

243
digitalclock/main_test.go Normal file
View file

@ -0,0 +1,243 @@
package main
import (
"encoding/json"
"fmt"
"image"
"image/png"
"io/ioutil"
"net/http"
"net/url"
"os"
"os/exec"
"path"
"strconv"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"gitlab.com/slon/shad-go/tools/testtool"
)
const importPath = "gitlab.com/slon/shad-go/digitalclock"
var binCache testtool.BinCache
func TestMain(m *testing.M) {
os.Exit(func() int {
var teardown testtool.CloseFunc
binCache, teardown = testtool.NewBinCache()
defer teardown()
return m.Run()
}())
}
func startServer(t *testing.T) (port string, stop func()) {
binary, err := binCache.GetBinary(importPath)
require.NoError(t, err)
port, err = testtool.GetFreePort()
require.NoError(t, err, "unable to get free port")
cmd := exec.Command(binary, "-port", port)
cmd.Stdout = nil
cmd.Stderr = os.Stderr
require.NoError(t, cmd.Start())
done := make(chan error)
go func() {
done <- cmd.Wait()
}()
stop = func() {
_ = cmd.Process.Kill()
<-done
}
if err = testtool.WaitForPort(t, time.Second*30, port); err != nil {
stop()
}
require.NoError(t, err)
return
}
func readImage(fname string) (image.Image, error) {
f, err := os.Open(fname)
if err != nil {
return nil, fmt.Errorf("unable to open %s: %w", fname, err)
}
defer func() { _ = f.Close() }()
img, _, err := image.Decode(f)
if err != nil {
return nil, fmt.Errorf("unable to decode %s: %w", fname, err)
}
return img, nil
}
func abs(a, b uint32) int64 {
if a > b {
return int64(a - b)
}
return int64(b - a)
}
func calcImgDiff(i1, i2 image.Image) float64 {
w, h := i1.Bounds().Dx(), i1.Bounds().Dy()
var sum int64
for x := 0; x < w; x++ {
for y := 0; y < h; y++ {
r1, g1, b1, _ := i1.At(x, y).RGBA()
r2, g2, b2, _ := i2.At(x, y).RGBA()
sum += abs(r1, r2)
sum += abs(g1, g2)
sum += abs(b1, b2)
}
}
return float64(sum) / (float64(w*h) * 0xffff * 3) * 100.0
}
func queryImage(t *testing.T, c *http.Client, url string) image.Image {
t.Logf("querying: %s", url)
resp, err := c.Get(url)
require.NoError(t, err)
defer func() { _ = resp.Body.Close() }()
require.Equal(t, http.StatusOK, resp.StatusCode)
require.Equal(t, "image/png", resp.Header.Get("Content-Type"))
img, err := png.Decode(resp.Body)
require.Nil(t, err)
return img
}
func TestDigitalClock_valid(t *testing.T) {
port, stop := startServer(t)
defer stop()
files, err := ioutil.ReadDir("./testdata")
require.Nil(t, err)
c := &http.Client{Timeout: time.Second * 10}
for _, f := range files {
t.Run(f.Name(), func(t *testing.T) {
parts := strings.SplitN(strings.TrimSuffix(f.Name(), ".png"), "_", 2)
v := url.Values{}
v.Add("time", parts[0])
v.Add("k", parts[1])
u := fmt.Sprintf("http://localhost:%s/?%s", port, v.Encode())
img := queryImage(t, c, u)
expected, err := readImage(path.Join("testdata", f.Name()))
require.NoError(t, err)
w, h := img.Bounds().Dx(), img.Bounds().Dy()
ew, eh := expected.Bounds().Dx(), expected.Bounds().Dy()
if w != ew || h != eh {
t.Errorf("expected size %d x %d got %d x %d", ew, eh, w, h)
}
diff := calcImgDiff(img, expected)
t.Logf("image diff: %.2f %%", diff)
require.True(t, diff < 1.0,
fmt.Sprintf("%s images are too different (%.2f %%)", f.Name(), diff))
})
}
}
func TestDigitalClock_invalid(t *testing.T) {
port, stop := startServer(t)
defer stop()
c := &http.Client{Timeout: time.Second * 10}
type TestCase struct {
Time string `json:"time"`
K string `json:"k"`
}
testName := func(tc *TestCase) string {
data, err := json.Marshal(tc)
require.Nil(t, err)
return string(data)
}
for _, tc := range []*TestCase{
{K: "0"},
{K: "31"},
{K: "2.5"},
{K: "f"},
{Time: "15:04"},
{Time: "15:04:0"},
{Time: "3:04:05"},
{Time: "15:4:05"},
{Time: "24:00:00"},
{Time: "00:00:60"},
{Time: "f"},
} {
t.Run(testName(tc), func(t *testing.T) {
v := url.Values{}
if tc.Time != "" {
v.Add("time", tc.Time)
}
if tc.K != "" {
v.Add("k", tc.K)
}
u := fmt.Sprintf("http://localhost:%s/?%s", port, v.Encode())
t.Logf("querying: %s", u)
resp, err := c.Get(u)
require.NoError(t, err)
defer func() { _ = resp.Body.Close() }()
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
}
}
func TestDigitalClock_now(t *testing.T) {
port, stop := startServer(t)
defer stop()
c := &http.Client{Timeout: time.Second * 10}
for _, k := range []int{1, 10, 29} {
k := k
t.Run(fmt.Sprintf("k=%d", k), func(t *testing.T) {
v := url.Values{"k": []string{strconv.Itoa(k)}}
u := fmt.Sprintf("http://localhost:%s/?%s", port, v.Encode())
img := queryImage(t, c, u)
w, h := img.Bounds().Dx(), img.Bounds().Dy()
ew, eh := getExpectedWidth(k), getExpectedHeight(k)
if w != ew || h != eh {
t.Errorf("expected size %d x %d got %d x %d", ew, eh, w, h)
}
})
}
}
func getSymbolWidth(s string) int {
return len(strings.SplitN(s, "\n", 2)[0])
}
func getExpectedWidth(k int) int {
return (getSymbolWidth(Zero)*6 + getSymbolWidth(Colon)*2) * k
}
func getExpectedHeight(k int) int {
return len(strings.Split(Zero, "\n")) * k
}

152
digitalclock/symbols.go Normal file
View file

@ -0,0 +1,152 @@
// +build !change
package main
import "image/color"
var Cyan = color.RGBA{R: 100, G: 200, B: 200, A: 0xff}
const (
Zero = `........
.111111.
.111111.
.11..11.
.11..11.
.11..11.
.11..11.
.11..11.
.11..11.
.111111.
.111111.
........`
One = `........
.....11.
.....11.
.....11.
.....11.
.....11.
.....11.
.....11.
.....11.
.....11.
.....11.
........`
Two = `........
.111111.
.111111.
.....11.
.....11.
.111111.
.111111.
.11.....
.11.....
.111111.
.111111.
........`
Three = `........
.111111.
.111111.
.....11.
.....11.
.111111.
.111111.
.....11.
.....11.
.111111.
.111111.
........`
Four = `........
.11..11.
.11..11.
.11..11.
.11..11.
.111111.
.111111.
.....11.
.....11.
.....11.
.....11.
........`
Five = `........
.111111.
.111111.
.11.....
.11.....
.111111.
.111111.
.....11.
.....11.
.111111.
.111111.
........`
Six = `........
.111111.
.111111.
.11.....
.11.....
.111111.
.111111.
.11..11.
.11..11.
.111111.
.111111.
........`
Seven = `........
.111111.
.111111.
.....11.
.....11.
.....11.
.....11.
.....11.
.....11.
.....11.
.....11.
........`
Eight = `........
.111111.
.111111.
.11..11.
.11..11.
.111111.
.111111.
.11..11.
.11..11.
.111111.
.111111.
........`
Nine = `........
.111111.
.111111.
.11..11.
.11..11.
.111111.
.111111.
.....11.
.....11.
.....11.
.....11.
........`
Colon = `....
....
....
....
.11.
.11.
....
.11.
.11.
....
....
....`
)

BIN
digitalclock/testdata/00:51:41_20.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
digitalclock/testdata/02:06:08_11.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 B

BIN
digitalclock/testdata/02:07:50_29.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
digitalclock/testdata/02:40:33_26.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
digitalclock/testdata/03:05:12_9.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 B

BIN
digitalclock/testdata/03:55:20_19.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
digitalclock/testdata/03:57:01_9.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 B

BIN
digitalclock/testdata/04:16:43_21.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
digitalclock/testdata/04:49:37_14.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 B

BIN
digitalclock/testdata/05:01:26_14.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 995 B

BIN
digitalclock/testdata/06:12:20_21.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
digitalclock/testdata/07:36:15_2.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 B

BIN
digitalclock/testdata/09:00:51_21.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
digitalclock/testdata/09:17:08_23.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
digitalclock/testdata/11:14:07_30.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
digitalclock/testdata/13:10:10_13.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 896 B

BIN
digitalclock/testdata/13:11:11_27.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
digitalclock/testdata/14:58:09_30.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
digitalclock/testdata/15:25:05_7.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 B

BIN
digitalclock/testdata/15:37:28_14.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 996 B

BIN
digitalclock/testdata/18:15:02_7.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 B

BIN
digitalclock/testdata/19:03:33_7.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 B

BIN
digitalclock/testdata/20:17:35_27.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
digitalclock/testdata/20:21:38_24.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

BIN
digitalclock/testdata/21:21:21_14.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 959 B

BIN
digitalclock/testdata/22:14:54_27.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
digitalclock/testdata/22:16:10_20.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
digitalclock/testdata/22:48:18_10.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 B

BIN
digitalclock/testdata/22:55:07_23.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
digitalclock/testdata/23:43:50_25.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

1
go.mod
View file

@ -3,6 +3,7 @@ module gitlab.com/slon/shad-go
go 1.13
require (
github.com/go-resty/resty/v2 v2.1.0
github.com/spf13/cobra v0.0.5
github.com/stretchr/testify v1.4.0
go.uber.org/goleak v1.0.0

7
go.sum
View file

@ -8,11 +8,15 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-resty/resty/v2 v2.1.0 h1:Z6IefCpUMfnvItVJaJXWv/pMiiD11So35QgwEELsldE=
github.com/go-resty/resty/v2 v2.1.0/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@ -48,6 +52,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -63,6 +69,7 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbO
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

99
urlshortener/README.md Normal file
View file

@ -0,0 +1,99 @@
## urlshortener
В этой задаче нужно написать http сервер со следующим API:
* POST /shorten {"url": "\<URL\>"} -> {"key": "\<KEY\>"}
* GET /go/\<KEY\> -> 302
GET и POST - это методы HTTP. GET запрос используется для того, чтобы получать данные, а POST - чтобы добавлять и модифицировать.
В тело `/shorten` запроса будет передаваться json вида
```
{"url":"https://github.com/golang/go/wiki/CodeReviewComments"}
```
Сервер должен ответить json'ом следующего вида:
```
{
"url": "https://github.com/golang/go/wiki/CodeReviewComments",
"key": "ed1De1"
}
```
`ed1De1` здесь - это сгенерированное сервисом число.
После такого `/shorten` можно делать `/go/ed1De1`.
Ответ должен иметь иметь HTTP код 302.
302 указывает на то, что запрошенный ресурс был временно перемещен на другой адрес (передаваемый в HTTP header'е `Location`).
Если открыть http://localhost:6029/go/ed1De1 в браузере, тот перенаправит на https://github.com/golang/go/wiki/CodeReviewComments.
Сервер должен слушать порт, переданный через аргумент `--port`.
### Примеры
Запуск:
```
$ urlshortener -port 6029
```
Успешное добавление URL'а (200, Content-Type: application/json):
```
$ curl -i -X POST "localhost:6029/shorten" -d '{"url":"https://github.com/golang/go/wiki/CodeReviewComments"}'
HTTP/1.1 200 OK
Content-Type: application/json
Date: Sat, 15 Feb 2020 23:35:26 GMT
Content-Length: 82
{"url":"https://github.com/golang/go/wiki/CodeReviewComments","key":"65ed150831"}
```
Невалидный json (400):
```
$ curl -i -X POST "localhost:6029/shorten" -d '{"url":"https://github.com'
HTTP/1.1 400 Bad Request
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Sat, 15 Feb 2020 23:30:27 GMT
Content-Length: 16
invalid request
```
Успешный запрос (302, Location header):
```
$ curl -i -X GET "localhost:6029/go/c1464c853a"
HTTP/1.1 302 Found
Content-Type: text/html; charset=utf-8
Location: https://github.com/golang/go/wiki/CodeReviewComments
Date: Sat, 15 Feb 2020 23:25:26 GMT
Content-Length: 75
<a href="https://github.com/golang/go/wiki/CodeReviewComments">Found</a>.
```
Несуществующий key (404):
```
$ curl -i -X GET "localhost:6029/go/uaaab"
HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Sat, 15 Feb 2020 23:26:48 GMT
Content-Length: 14
key not found
```
### Состояние
Своё состояние сервис должен целиком хранить в памяти.
Стандартный http server на каждый запрос запускает handler в отдельной горутине (https://golang.org/pkg/net/http/#Serve),
поэтому доступ к состоянию нужно защитить. Например, это можно сделать с помощью [мьютекса](https://golang.org/pkg/sync/#Mutex).
## Ссылки
1. Пример web сервера и работы с общим состоянием: https://p.go.manytask.org/00-intro/lecture.slide#24
2. протокол HTTP: https://ru.wikipedia.org/wiki/HTTP
3. http multiplexer: https://golang.org/pkg/net/http/#ServeMux
4. десериализация json'а: https://golang.org/pkg/encoding/json/#example_Unmarshal
5. генерация случайных данных: https://golang.org/pkg/math/rand/

7
urlshortener/main.go Normal file
View file

@ -0,0 +1,7 @@
// +build !solution
package main
func main() {
}

200
urlshortener/main_test.go Normal file
View file

@ -0,0 +1,200 @@
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"reflect"
"sync"
"testing"
"time"
"github.com/go-resty/resty/v2"
"github.com/stretchr/testify/require"
"gitlab.com/slon/shad-go/tools/testtool"
)
const importPath = "gitlab.com/slon/shad-go/urlshortener"
var binCache testtool.BinCache
func TestMain(m *testing.M) {
os.Exit(func() int {
var teardown testtool.CloseFunc
binCache, teardown = testtool.NewBinCache()
defer teardown()
return m.Run()
}())
}
func startServer(t *testing.T) (port string, stop func()) {
binary, err := binCache.GetBinary(importPath)
require.NoError(t, err)
port, err = testtool.GetFreePort()
require.NoError(t, err, "unable to get free port")
cmd := exec.Command(binary, "-port", port)
cmd.Stdout = nil
cmd.Stderr = os.Stderr
require.NoError(t, cmd.Start())
done := make(chan error)
go func() {
done <- cmd.Wait()
}()
stop = func() {
_ = cmd.Process.Kill()
<-done
}
if err = testtool.WaitForPort(t, time.Second*5, port); err != nil {
stop()
}
require.NoError(t, err)
return
}
func add(t *testing.T, c *resty.Client, shortenerURL, request string) string {
type Response struct {
URL string `json:"url"`
Key string `json:"key"`
}
resp, err := c.R().
SetBody(map[string]interface{}{"url": request}).
SetResult(&Response{}).
Post(shortenerURL)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode())
response := resp.Result().(*Response)
require.Equal(t, request, response.URL)
require.Contains(t, resp.Header().Get("Content-Type"), "application/json")
return response.Key
}
func TestURLShortener_redirect(t *testing.T) {
port, stop := startServer(t)
defer stop()
var mu sync.Mutex
redirects := make(map[string]struct{})
redirectTarget := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
redirects[r.RequestURI] = struct{}{}
mu.Unlock()
_, _ = fmt.Fprintln(w, "hello")
}))
client := resty.New()
addURL := fmt.Sprintf("http://localhost:%s/shorten", port)
requests := make(map[string]struct{})
for i := 0; i < 10; i++ {
path := "/" + testtool.RandomName()
req := redirectTarget.URL + path
requests[path] = struct{}{}
key := add(t, client, addURL, req)
getURL := fmt.Sprintf("http://localhost:%s/go/%s", port, key)
resp, err := client.R().Get(getURL)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode())
}
mu.Lock()
defer mu.Unlock()
require.True(t, reflect.DeepEqual(requests, redirects),
fmt.Sprintf("expected: %+v, got: %+v", requests, redirects))
}
func TestURLShortener_badRequest(t *testing.T) {
port, stop := startServer(t)
defer stop()
u := fmt.Sprintf("http://localhost:%s/shorten", port)
resp, err := resty.New().R().
SetHeader("Content-Type", "application/json").
SetBody([]byte(`{"url":"abc}`)).
Post(u)
require.NoError(t, err)
require.Equal(t, http.StatusBadRequest, resp.StatusCode())
}
func TestURLShortener_badKey(t *testing.T) {
port, stop := startServer(t)
defer stop()
u := fmt.Sprintf("http://localhost:%s/go/%s", port, testtool.RandomName())
resp, err := resty.New().
SetRedirectPolicy(resty.RedirectPolicyFunc(func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
})).R().
Get(u)
require.NoError(t, err)
require.Equal(t, http.StatusNotFound, resp.StatusCode())
}
func TestURLShortener_consistency(t *testing.T) {
port, stop := startServer(t)
defer stop()
client := resty.New()
get := func(originalURL, key string) {
getURL := fmt.Sprintf("http://localhost:%s/go/%s", port, key)
resp, err := client.
SetRedirectPolicy(resty.RedirectPolicyFunc(func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
})).R().
Get(getURL)
require.NoError(t, err)
require.Equal(t, http.StatusFound, resp.StatusCode())
require.Contains(t, resp.Header().Get("Location"), originalURL)
}
var urls []string
for i := 0; i < 10; i++ {
urls = append(urls, testtool.RandomName())
}
keyToURL := make(map[string]string)
urlToKey := make(map[string]string)
addURL := fmt.Sprintf("http://localhost:%s/shorten", port)
for _, u := range urls {
key := add(t, client, addURL, u)
url, ok := keyToURL[key]
require.False(t, ok, fmt.Sprintf("duplicate key %s for urls [%s, %s]", key, u, url))
urlToKey[u] = key
keyToURL[key] = u
}
for _, u := range urls {
get(u, urlToKey[u])
}
for _, u := range urls {
key := add(t, client, addURL, u)
url, ok := keyToURL[key]
require.True(t, ok, fmt.Sprintf("different keys for the same url %s", u))
require.Equal(t, url, u)
}
}