Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/go-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Go Test

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22.x'

- name: Test
run: go test ./...
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,6 @@ go.work.sum
.vscode/
*.~

# Database files
api/*.db

9 changes: 4 additions & 5 deletions api/api.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
package api

import (
"database/sql"
"log"
"net/http"

_ "modernc.org/sqlite"
"lidsol.org/papeador/store"
)

type ApiContext struct{
DB *sql.DB
Store store.Store
}

func API(sqlitedb *sql.DB) {
func API(s store.Store) {
apiCtx := ApiContext{
DB: sqlitedb,
Store: s,
}

mux := http.NewServeMux()
Expand Down
30 changes: 18 additions & 12 deletions api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import (
"net/http"
"net/http/httptest"
"os"
"os/exec"
"testing"

"lidsol.org/papeador/store"

_ "modernc.org/sqlite"
)

Expand All @@ -23,20 +24,24 @@ func TestMain(m *testing.M) {
log.Fatal(err)
}
testDB = db
defer os.Remove("test_api.db") // clean up

// run schema
command := exec.Command("bash", "-c", "sqlite3 test_api.db < ../schema.sql")
output, err := command.CombinedOutput()
schemaBytes, err := os.ReadFile("../schema.sql")
if err != nil {
log.Fatalf("Failed to run schema: %s, %s", err, string(output))
log.Fatalf("Failed to read schema.sql: %v", err)
}
if _, err := db.Exec(string(schemaBytes)); err != nil {
log.Fatalf("Failed to execute schema: %v", err)
}

// Run tests
exitCode := m.Run()

// Teardown: close the database connection
testDB.Close()
// remove the temporary DB file
if err := os.Remove("test_api.db"); err != nil {
log.Printf("Warning: failed to remove test DB: %v", err)
}

os.Exit(exitCode)
}
Expand All @@ -45,7 +50,9 @@ func TestMain(m *testing.M) {

func executeRequest(req *http.Request) *httptest.ResponseRecorder {
rr := httptest.NewRecorder()
handler := http.HandlerFunc(createUser)
// create an ApiContext that uses the test sqlite DB
apiCtx := ApiContext{Store: store.NewSQLiteStore(testDB)}
handler := http.HandlerFunc(apiCtx.createUser)
handler.ServeHTTP(rr, req)
return rr
}
Expand All @@ -58,7 +65,7 @@ func checkStatus(t *testing.T, rr *httptest.ResponseRecorder, expectedStatus int
}
}

func createUserRequest(user User) (*http.Request, error) {
func createUserRequest(user store.User) (*http.Request, error) {
jsonUser, _ := json.Marshal(user)
return http.NewRequest("POST", "/users", bytes.NewBuffer(jsonUser))
}
Expand All @@ -72,10 +79,9 @@ func clearUserTable() {

func TestCreateUser(t *testing.T) {
// Initialize the API with the test database
db = testDB
// apiCtx is constructed inside executeRequest per-call using testDB
clearUserTable()

newUser := User{
newUser := store.User{
Username: "testuser",
Passhash: "testpass",
Email: "[email protected]",
Expand All @@ -90,7 +96,7 @@ func TestCreateUser(t *testing.T) {
checkStatus(t, rr, http.StatusCreated)

// Check the response body
var createdUser User
var createdUser store.User
err = json.NewDecoder(rr.Body).Decode(&createdUser)
if err != nil {
t.Fatal(err)
Expand Down
47 changes: 11 additions & 36 deletions api/contest.go
Original file line number Diff line number Diff line change
@@ -1,57 +1,32 @@
package api

import (
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
)

type Contest struct {
ContestID int `json:"contest_id"`
ContestName string `json:"contest_name"`
}
"lidsol.org/papeador/store"
)

func (api *ApiContext) createContest(w http.ResponseWriter, r *http.Request) {
var newContest Contest
if err := json.NewDecoder(r.Body).Decode(&newContest); err != nil {
var in store.Contest
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

// Verificar si el nombre del concurso ya existe
var contestName string
query := "SELECT contest_name FROM contest WHERE contest_name=?"
err := api.DB.QueryRow(query, newContest.ContestName).Scan(&contestName)
if err == nil {
// existe
http.Error(w, "El nombre del contest ya esta registrado", http.StatusConflict)
log.Println("El nombre del contest ya esta registrado")
return
} else if err != sql.ErrNoRows {
// error distinto a no rows
if err := api.Store.CreateContest(r.Context(), &in); err != nil {
if err == store.ErrAlreadyExists {
http.Error(w, "El nombre del contest ya esta registrado", http.StatusConflict)
log.Println("El nombre del contest ya esta registrado")
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println(err)
return
}

res, err := api.DB.Exec(
"INSERT INTO contest (contest_name) VALUES (?)",
newContest.ContestName,
)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println(err)
return
}

// Intentar obtener el id generado
if id, ierr := res.LastInsertId(); ierr == nil {
newContest.ContestID = int(id)
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(newContest)
json.NewEncoder(w).Encode(in)
}
40 changes: 7 additions & 33 deletions api/problem.go
Original file line number Diff line number Diff line change
@@ -1,58 +1,32 @@
package api

import (
"database/sql"
"encoding/json"
"log"
"net/http"
)

type Problem struct {
ProblemID int `json:"problem_id"`
ContestID *int `json:"contest_id"`
CreatorID int `json:"creator_id"`
ProblemName string `json:"problem_name"`
Description string `json:"description"`
}
"lidsol.org/papeador/store"
)

func (api *ApiContext) createProblem(w http.ResponseWriter, r *http.Request) {
var newProblem Problem
if err := json.NewDecoder(r.Body).Decode(&newProblem); err != nil {
var in store.Problem
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

if newProblem.ContestID != nil {
var contestID int
query := "SELECT contest_id FROM CONTEST WHERE contest_id=?"
err := api.DB.QueryRow(query, *newProblem.ContestID).Scan(&contestID)
if err == sql.ErrNoRows {
if err := api.Store.CreateProblem(r.Context(), &in); err != nil {
if err == store.ErrNotFound {
http.Error(w, "El concurso no existe", http.StatusConflict)
log.Println("El concurso no existe")
return
} else if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println(err)
return
}
}

res, err := api.DB.Exec(
"INSERT INTO PROBLEM (contest_id, creator_id, problem_name, description) VALUES (?, ?, ?, ?)",
newProblem.ContestID, newProblem.CreatorID, newProblem.ProblemName, newProblem.Description,
)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println(err)
return
}

// Obtener el ID del problema recién insertado
if id, err := res.LastInsertId(); err == nil {
newProblem.ProblemID = int(id)
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(newProblem)
json.NewEncoder(w).Encode(in)
}
45 changes: 11 additions & 34 deletions api/user.go
Original file line number Diff line number Diff line change
@@ -1,55 +1,32 @@
package api

import (
"database/sql"
"encoding/json"
"log"
"net/http"
)

type User struct {
UserID int `json:"user_id"`
Username string `json:"username"`
Passhash string `json:"passhash"`
Email string `json:"email"`
}
"lidsol.org/papeador/store"
)

func (api *ApiContext) createUser(w http.ResponseWriter, r *http.Request) {
var new_user User
var in store.User

err := json.NewDecoder(r.Body).Decode(&new_user)
if err != nil {
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

// Verify if the user already exists (unique username and email)
var username string
query := "SELECT username FROM user WHERE username=? OR email=?"
err = api.DB.QueryRow(query, new_user.Username, new_user.Email).Scan(&username)
if err == nil {
http.Error(w, "El usuario ya está registrado", http.StatusConflict)
log.Println("El usuario ya está registrado")
return
} else if err != sql.ErrNoRows {
if err := api.Store.CreateUser(r.Context(), &in); err != nil {
if err == store.ErrAlreadyExists {
http.Error(w, "El usuario ya está registrado", http.StatusConflict)
log.Println("El usuario ya está registrado")
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println(err)
return
}

res, err := api.DB.Exec("INSERT INTO user (username,passhash,email) VALUES (?, ?, ?)", new_user.Username, new_user.Passhash, new_user.Email)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println(err)
return
}

// Obtener el ID del usuario recién insertado
if id, err := res.LastInsertId(); err == nil {
new_user.User_ID = int(id)
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(new_user)
json.NewEncoder(w).Encode(in)
}
4 changes: 3 additions & 1 deletion papeador.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os/exec"

"lidsol.org/papeador/api"
"lidsol.org/papeador/store"
_ "modernc.org/sqlite"
)

Expand All @@ -23,5 +24,6 @@ func main() {
fmt.Println(string(output))
log.Fatal(err)
}
api.API(db)
s := store.NewSQLiteStore(db)
api.API(s)
}
Loading