diff --git a/.github/workflows/api.yaml b/.github/workflows/api.yaml new file mode 100644 index 0000000..f9a40e3 --- /dev/null +++ b/.github/workflows/api.yaml @@ -0,0 +1,24 @@ +name: api + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.16 + + - name: Build + run: | + cd api/src + go build -v . + + - name: Test + run: | + cd api/src + go test -v . \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..107b6ef --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: go + +go: + - 1.x + - 1.16 + +script: + - cd api/src + - go test -v . + - go build diff --git a/README.md b/README.md index 80121ad..c275310 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +[![Build Status](https://travis-ci.org/Coding-Web-Community/CodingBump.svg?branch=main)](https://travis-ci.org/Coding-Web-Community/CodingBump) # CodingBump Disboard clone (for now) diff --git a/api/README.md b/api/README.md index 76f4f2c..e0ae247 100644 --- a/api/README.md +++ b/api/README.md @@ -1,3 +1,4 @@ +[![Build Status](https://travis-ci.org/Coding-Web-Community/CodingBump.svg?branch=master)](https://travis-ci.org/Coding-Web-Community/CodingBump) # Central API ## Usage @@ -17,7 +18,7 @@ go run . This is where we describe the API endpoints and how they react to certain data. -## bump +# bump **URL** Structure: ``` @@ -31,7 +32,7 @@ Method: **POST** {"guildId": 636145886279237652} ``` -# **Responses**: +## **Responses**: ### **200** @@ -112,3 +113,40 @@ allowed characters: 0-9 *Additional note*: The **payload** is always a direct and latest representation of the stored guild in the database. That means when getting a `200` or `425` status code, the `timestamp` attribute represents the time that guild was last bumped in a UNIX timestamp. + +# fetch + +**URL** Structure: +``` +http://localhost:8080/V1/fetch +``` + +Method: **GET** + +## **Responses**: + + +### **200** +- *200 - Ok* | **guildId** successfully bumped! +```json +{ + "code":200, + "message":"Ok", + "payload":[ + {"guildId":636145886279237699,"timestamp":1602394289}, + {"guildId":636123886245557612,"timestamp":1602394230} + ] +} +``` + +### **400** +- *400 - BadRequest* +```json +{ + "code":400, + "message":"BadRequest", + "payload":[ + {} + ] + } +``` diff --git a/api/src/bump_test.go b/api/src/bump_test.go new file mode 100644 index 0000000..2a44d87 --- /dev/null +++ b/api/src/bump_test.go @@ -0,0 +1,202 @@ +package main + +import ( + "bytes" + "crypto/md5" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "os" + "testing" + "time" +) + +func startServer() { + path := "store.json" + err := os.Remove(path) + if err != nil { + fmt.Println(err) + } + + go HandleRequests() +} + +var ( + bumpRoute = fmt.Sprintf("http://%v%v/V1/bump",URL ,PORT) + fetchRoute = fmt.Sprintf("http://%v%v/V1/fetch",URL ,PORT) +) + +func sendInt(guildId int) (br BumpResponse) { + reqBody, _ := json.Marshal(map[string]int{ + "guildId": guildId, + }) + + resp, _ := http.Post(bumpRoute, "application/json", bytes.NewBuffer(reqBody)) + + defer resp.Body.Close() + body, _ := ioutil.ReadAll(resp.Body) + + _ = json.Unmarshal(body, &br) + + return br +} + +func sendString(guildId string) (br BumpResponse) { + reqBody, _ := json.Marshal(map[string]string{ + "guildId": guildId, + }) + + resp, _ := http.Post(bumpRoute, "application/json", bytes.NewBuffer(reqBody)) + + defer resp.Body.Close() + body, _ := ioutil.ReadAll(resp.Body) + + _ = json.Unmarshal(body, &br) + + return br +} + +func Hash(guilds []Guild) [16]byte { + guildBytes := []byte{} + for _, item := range guilds { + jsonBytes, _ := json.Marshal(item) + guildBytes = append(guildBytes, jsonBytes...) + } + return md5.Sum(guildBytes) +} + +func TestServerStart(t *testing.T) { + go startServer() + + TempTestInterval = 1 + Logging = false + + time.Sleep(time.Millisecond * 200) +} + +func TestBumpNormal(t *testing.T) { + var guildId int = 636145886279237611 + + var expected = BumpResponse{ + Code: 200, // OK + Payload: Guild{ + GuildId: guildId, + }, + } + + // Normal send, returns 200 + resp := sendInt(guildId) + + if resp.Code != expected.Code { + t.Errorf("Status codes were not equal: %v != %v", resp.Code, expected.Code) + } + if resp.Payload.GuildId != expected.Payload.GuildId { + t.Errorf("GuilId's were not equal: %v != %v", resp.Payload.GuildId, expected.Payload.GuildId) + } + +} + +func TestBumpEarly(t *testing.T) { + var guildId int = 636145886279237699 + + _ = sendInt(guildId) // send first bump request + + var expected = BumpResponse{ + Code: 425, // Too Early + Payload: Guild{ + GuildId: guildId, + }, + } + + resp := sendInt(guildId) // send second (early) bump request + + if resp.Code != expected.Code { + t.Errorf("Status codes were not equal: %v != %v", resp.Code, expected.Code) + } + + time.Sleep(time.Second * 2) + + expected = BumpResponse{ + Code: 200, // OK + Payload: Guild{ + GuildId: guildId, + }, + } + + resp = sendInt(guildId) // send third (late) bump request! + + if resp.Code != expected.Code { + t.Errorf("Status codes were not equal: %v != %v", resp.Code, expected.Code) + } + if resp.Payload.GuildId != expected.Payload.GuildId { + t.Errorf("GuilId's were not equal: %v != %v", resp.Payload.GuildId, expected.Payload.GuildId) + } + +} + +func TestBumpString(t *testing.T) { + var expected = BumpResponse{ + Code: 400, // Bad Request + Message: "Unable to process request body", + Payload: Guild{ + GuildId: 0, + }, + } + + resp := sendString("636145886279237652") + + if resp.Code != expected.Code { + t.Errorf("Status codes were not equal: %v != %v", resp.Code, expected.Code) + } + if resp.Payload.GuildId != expected.Payload.GuildId { + t.Errorf("GuilId's were not equal: %v != %v", resp.Payload.GuildId, expected.Payload.GuildId) + } + if resp.Message != expected.Message { + t.Errorf("Messages were not equal: %v != %v", resp.Message, expected.Message) + } +} + +func TestBumpTooFewChars(t *testing.T) { + var guildId int = 636145 + var expected = BumpResponse{ + Code: 400, // Bad Request + Message: "GuildId does not conform to 18 character long integer requirement", + Payload: Guild{ + GuildId: guildId, + }, + } + + resp := sendInt(guildId) + + if resp.Code != expected.Code { + t.Errorf("Status codes were not equal: %v != %v", resp.Code, expected.Code) + } + if resp.Payload.GuildId != expected.Payload.GuildId { + t.Errorf("GuilId's were not equal: %v != %v", resp.Payload.GuildId, expected.Payload.GuildId) + } + if resp.Message != expected.Message { + t.Errorf("Messages were not equal: %v != %v", resp.Message, expected.Message) + } +} + +func TestFetch(t *testing.T) { + var fr FetchResponse + guilds, _ := LoadStore() + + resp, _ := http.Get(fetchRoute) + + defer resp.Body.Close() + body, _ := ioutil.ReadAll(resp.Body) + + _ = json.Unmarshal(body, &fr) + + if len(fr.Payload) != len(guilds) { + t.Errorf("Length of stored guilds not equal to length of /V1/fetch result: %v != %v", len(fr.Payload), len(guilds)) + } + + if Hash(fr.Payload) != Hash(guilds) { + t.Errorf("Guilds hash from GuildStore are not equal to /V1/fetch hash: %v != %v", Hash(fr.Payload), Hash(guilds)) + } + +} diff --git a/api/src/fetch.go b/api/src/fetch.go new file mode 100644 index 0000000..2284e7c --- /dev/null +++ b/api/src/fetch.go @@ -0,0 +1,17 @@ +package main + +import ( + "net/http" +) + +func FetchGuilds(w http. + ResponseWriter, r *http.Request) { + guilds := gs.GetGuilds() + + if len(guilds) > 0 { + WriteFetchResponse(w, 200, "Ok", guilds) + return + } + + WriteFetchResponse(w, 400, "BadRequest", guilds) +} diff --git a/api/src/main.go b/api/src/main.go index ca6f63f..42e8430 100644 --- a/api/src/main.go +++ b/api/src/main.go @@ -11,12 +11,17 @@ import ( ) const ( + URL = "localhost" PORT = ":8080" BUMP_INTERVAL = 60 // 1 minute in seconds STORE_FILE_NAME = "store.json" ) -var gs GuildStore +var ( + TempTestInterval = 0 // used to set lower interval during testing + Logging = true // used to disable logging during testing + gs GuildStore +) type Guild struct { GuildId int `json:"guildId"` @@ -34,6 +39,12 @@ type BumpResponse struct { Payload Guild `json:"payload"` } +type FetchResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Payload []Guild `json:"paypload"` +} + func init() { var err error gs.Guilds, err = LoadStore() @@ -47,7 +58,9 @@ func init() { func middleware(f http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - log.Print(fmt.Sprintf("%s%s - %s", r.Host, r.URL.Path, r.Method)) + if Logging { + log.Print(fmt.Sprintf("%s%s - %s", r.Host, r.URL.Path, r.Method)) + } f(w, r) } } @@ -55,7 +68,9 @@ func middleware(f http.HandlerFunc) http.HandlerFunc { func HandleRequests() { router := mux.NewRouter().StrictSlash(true) router.HandleFunc("/V1/bump", middleware(BumpGuild)).Methods("POST") - log.Print(fmt.Sprintf("Now serving: localhost%s", PORT)) + router.HandleFunc("/V1/fetch", middleware(FetchGuilds)).Methods("GET") + + log.Print(fmt.Sprintf("Now serving: %s%s",URL ,PORT)) err := http.ListenAndServe(PORT, router) if err != nil { log.Print(err) @@ -63,17 +78,31 @@ func HandleRequests() { } // makes BumpResponse object and writes it to ResponseWriter -func WriteBumpResponse(w http.ResponseWriter, code int, message string, guild Guild) { +func WriteBumpResponse(w http.ResponseWriter, code int, message string, payload Guild) { br := BumpResponse{ Code: code, Message: message, - Payload: guild, + Payload: payload, } payloadByte, _ := json.Marshal(br) + w.WriteHeader(code) + w.Header().Set("Content-Type", "application/json") + w.Write(payloadByte) +} + +func WriteFetchResponse(w http.ResponseWriter, code int, message string, payload []Guild) { + fr := FetchResponse{ + Code: code, + Message: message, + Payload: payload, + } + + payloadByte, _ := json.Marshal(fr) + + w.WriteHeader(code) w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) w.Write(payloadByte) } diff --git a/api/src/store.go b/api/src/store.go index 421cc3c..a3c4853 100644 --- a/api/src/store.go +++ b/api/src/store.go @@ -95,7 +95,14 @@ func (gs *GuildStore) PastInterval(guild Guild) bool { if gsGuild.GuildId == guild.GuildId { ts := time.Now().Unix() - if (ts - gsGuild.Timestamp) >= BUMP_INTERVAL { + var interval int64 + if TempTestInterval == 0 { + interval = BUMP_INTERVAL + } else { + interval = int64(TempTestInterval) + } + + if (ts - gsGuild.Timestamp) >= interval { gs.Guilds[i].Timestamp = ts return true @@ -122,3 +129,16 @@ func (gs *GuildStore) GetTimestamp(guild Guild) int64 { return 0 } + +// returns guilds from guildstore +// if there are less than 10 guilds, it returns all guilds. +func (gs *GuildStore) GetGuilds() (guilds []Guild) { + gs.mutex.Lock() + defer gs.mutex.Unlock() + + if len(gs.Guilds) >= 10 { + return gs.Guilds[0:9] + } else { + return gs.Guilds[0:(len(gs.Guilds))] + } +}