320 lines
9.3 KiB
Go
320 lines
9.3 KiB
Go
package usostest
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha1"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
ConsumerKey = "fake-consumer-key"
|
|
ConsumerSecret = "fake-consumer-secret"
|
|
UserID = "1001"
|
|
)
|
|
|
|
var (
|
|
warsaw = mustLocation()
|
|
headerPair = regexp.MustCompile(`(oauth_[a-z_]+)="([^"]*)"`)
|
|
)
|
|
|
|
func mustLocation() *time.Location {
|
|
location, err := time.LoadLocation("Europe/Warsaw")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return location
|
|
}
|
|
|
|
type Class struct {
|
|
MeetingID int
|
|
Start time.Time
|
|
Minutes int
|
|
Course string
|
|
Kind string
|
|
Room string
|
|
}
|
|
|
|
type Fake struct {
|
|
*httptest.Server
|
|
|
|
mu sync.Mutex
|
|
Schedule func(day time.Time) []Class
|
|
Windows []string
|
|
Revoked bool
|
|
Deauthorize int
|
|
secrets map[string]string
|
|
callbacks map[string]string
|
|
counter int
|
|
}
|
|
|
|
func New() *Fake {
|
|
f := &Fake{secrets: map[string]string{}, callbacks: map[string]string{}, Schedule: Weekly}
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("POST /services/oauth/request_token", f.requestToken)
|
|
mux.HandleFunc("GET /services/oauth/authorize", f.authorize)
|
|
mux.HandleFunc("POST /services/oauth/access_token", f.accessToken)
|
|
mux.HandleFunc("POST /services/oauth/revoke_token", f.revokeToken)
|
|
mux.HandleFunc("POST /services/users/user", f.user)
|
|
mux.HandleFunc("POST /services/users/users", f.users)
|
|
mux.HandleFunc("POST /services/geo/building2", f.building)
|
|
mux.HandleFunc("POST /services/terms/search", f.terms)
|
|
mux.HandleFunc("POST /services/tt/user", f.timetable)
|
|
f.Server = httptest.NewServer(mux)
|
|
return f
|
|
}
|
|
|
|
func Weekly(day time.Time) []Class {
|
|
inTerm := func(from, to string) bool {
|
|
date := day.Format("2006-01-02")
|
|
return date >= from && date <= to
|
|
}
|
|
if !inTerm("2025-10-01", "2026-01-28") && !inTerm("2026-03-02", "2026-06-20") && !inTerm("2026-10-01", "2027-01-29") {
|
|
return nil
|
|
}
|
|
id, _ := strconv.Atoi(day.Format("20060102"))
|
|
at := func(hour, minute int) time.Time {
|
|
return time.Date(day.Year(), day.Month(), day.Day(), hour, minute, 0, 0, warsaw)
|
|
}
|
|
switch day.Weekday() {
|
|
case time.Wednesday:
|
|
return []Class{{MeetingID: id, Start: at(13, 30), Minutes: 90, Course: "Algebra liniowa z elementami matematyki dyskretnej", Kind: "Ćwiczenia", Room: "303"}}
|
|
case time.Friday:
|
|
return []Class{{MeetingID: id, Start: at(11, 45), Minutes: 90, Course: "Algebra liniowa z elementami matematyki dyskretnej", Kind: "Laboratorium", Room: "602"}}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *Fake) SetSchedule(schedule func(day time.Time) []Class) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.Schedule = schedule
|
|
}
|
|
|
|
func (f *Fake) WindowCount() int {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return len(f.Windows)
|
|
}
|
|
|
|
func (f *Fake) Revoke() {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.Revoked = true
|
|
}
|
|
|
|
func (f *Fake) WasRevoked() bool {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return f.Revoked
|
|
}
|
|
|
|
func percentEncode(s string) string {
|
|
return strings.NewReplacer("+", "%20", "*", "%2A", "%7E", "~").Replace(url.QueryEscape(s))
|
|
}
|
|
|
|
func (f *Fake) verify(w http.ResponseWriter, r *http.Request, needToken bool) (url.Values, bool) {
|
|
deny := func(status int, message string) (url.Values, bool) {
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"message": message})
|
|
return nil, false
|
|
}
|
|
if err := r.ParseForm(); err != nil {
|
|
return deny(http.StatusBadRequest, "bad form")
|
|
}
|
|
oauth := url.Values{}
|
|
for _, match := range headerPair.FindAllStringSubmatch(r.Header.Get("Authorization"), -1) {
|
|
value, _ := url.QueryUnescape(match[2])
|
|
oauth.Set(match[1], value)
|
|
}
|
|
if oauth.Get("oauth_consumer_key") != ConsumerKey {
|
|
return deny(http.StatusUnauthorized, "Invalid consumer.")
|
|
}
|
|
token := oauth.Get("oauth_token")
|
|
f.mu.Lock()
|
|
tokenSecret, known := f.secrets[token]
|
|
revoked := f.Revoked
|
|
f.mu.Unlock()
|
|
if needToken && (!known || revoked) {
|
|
return deny(http.StatusUnauthorized, "Invalid access token.")
|
|
}
|
|
var pairs []string
|
|
for key, values := range r.PostForm {
|
|
for _, value := range values {
|
|
pairs = append(pairs, percentEncode(key)+"="+percentEncode(value))
|
|
}
|
|
}
|
|
for key := range oauth {
|
|
if key != "oauth_signature" {
|
|
pairs = append(pairs, percentEncode(key)+"="+percentEncode(oauth.Get(key)))
|
|
}
|
|
}
|
|
sort.Strings(pairs)
|
|
base := "POST&" + percentEncode(f.URL+r.URL.Path) + "&" + percentEncode(strings.Join(pairs, "&"))
|
|
mac := hmac.New(sha1.New, []byte(percentEncode(ConsumerSecret)+"&"+percentEncode(tokenSecret)))
|
|
mac.Write([]byte(base))
|
|
if base64.StdEncoding.EncodeToString(mac.Sum(nil)) != oauth.Get("oauth_signature") {
|
|
return deny(http.StatusUnauthorized, "Invalid signature.")
|
|
}
|
|
for key, values := range oauth {
|
|
r.PostForm[key] = values
|
|
}
|
|
return r.PostForm, true
|
|
}
|
|
|
|
func (f *Fake) issue(prefix string) (key, secret string) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.counter++
|
|
key, secret = fmt.Sprintf("%s-token-%d", prefix, f.counter), fmt.Sprintf("%s-secret-%d", prefix, f.counter)
|
|
f.secrets[key] = secret
|
|
return key, secret
|
|
}
|
|
|
|
func (f *Fake) requestToken(w http.ResponseWriter, r *http.Request) {
|
|
form, ok := f.verify(w, r, false)
|
|
if !ok {
|
|
return
|
|
}
|
|
if form.Get("scopes") != "studies|offline_access" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
key, secret := f.issue("request")
|
|
f.mu.Lock()
|
|
f.callbacks[key] = form.Get("oauth_callback")
|
|
f.mu.Unlock()
|
|
fmt.Fprintf(w, "oauth_token=%s&oauth_token_secret=%s&oauth_callback_confirmed=true", key, secret)
|
|
}
|
|
|
|
func (f *Fake) authorize(w http.ResponseWriter, r *http.Request) {
|
|
token := r.URL.Query().Get("oauth_token")
|
|
f.mu.Lock()
|
|
callback := f.callbacks[token]
|
|
f.mu.Unlock()
|
|
if callback == "" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
http.Redirect(w, r, callback+"?oauth_token="+token+"&oauth_verifier=verifier-of-"+token, http.StatusFound)
|
|
}
|
|
|
|
func (f *Fake) accessToken(w http.ResponseWriter, r *http.Request) {
|
|
form, ok := f.verify(w, r, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
if form.Get("oauth_verifier") != "verifier-of-"+form.Get("oauth_token") {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
key, secret := f.issue("access")
|
|
fmt.Fprintf(w, "oauth_token=%s&oauth_token_secret=%s", key, secret)
|
|
}
|
|
|
|
func (f *Fake) revokeToken(w http.ResponseWriter, r *http.Request) {
|
|
if _, ok := f.verify(w, r, true); !ok {
|
|
return
|
|
}
|
|
f.Revoke()
|
|
_ = json.NewEncoder(w).Encode(map[string]bool{"success": true})
|
|
}
|
|
|
|
func (f *Fake) user(w http.ResponseWriter, r *http.Request) {
|
|
if _, ok := f.verify(w, r, true); !ok {
|
|
return
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"id": UserID, "first_name": "Ada", "last_name": "Nowak"})
|
|
}
|
|
|
|
func (f *Fake) users(w http.ResponseWriter, r *http.Request) {
|
|
form, ok := f.verify(w, r, false)
|
|
if !ok {
|
|
return
|
|
}
|
|
people := map[string]any{}
|
|
for _, id := range strings.Split(form.Get("user_ids"), "|") {
|
|
people[id] = map[string]any{
|
|
"id": id, "first_name": "Jan", "last_name": "Kowalski",
|
|
"titles": map[string]any{"before": "dr inż.", "after": nil},
|
|
}
|
|
}
|
|
_ = json.NewEncoder(w).Encode(people)
|
|
}
|
|
|
|
func (f *Fake) building(w http.ResponseWriter, r *http.Request) {
|
|
if _, ok := f.verify(w, r, false); !ok {
|
|
return
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"id": "A1",
|
|
"name": map[string]string{"pl": "A1 - BM - Budowa Maszyn, budynek z zegarem", "en": ""},
|
|
"postal_address": "ul. Piotrowo 3, 61-138 Poznań, Polska",
|
|
})
|
|
}
|
|
|
|
func (f *Fake) terms(w http.ResponseWriter, r *http.Request) {
|
|
if _, ok := f.verify(w, r, false); !ok {
|
|
return
|
|
}
|
|
_ = json.NewEncoder(w).Encode([]map[string]string{
|
|
{"id": "2026", "start_date": "2026-10-01", "end_date": "2027-09-30", "finish_date": "2027-09-30"},
|
|
{"id": "2026Z", "start_date": "2026-10-01", "end_date": "2027-01-29", "finish_date": "2027-02-28"},
|
|
})
|
|
}
|
|
|
|
func (f *Fake) timetable(w http.ResponseWriter, r *http.Request) {
|
|
form, ok := f.verify(w, r, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
days, _ := strconv.Atoi(form.Get("days"))
|
|
start, err := time.ParseInLocation("2006-01-02", form.Get("start"), warsaw)
|
|
if err != nil || days < 1 || days > 7 {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"message": "Parameter 'days' has invalid value."})
|
|
return
|
|
}
|
|
if strings.Contains(form.Get("fields"), "related_user_ids") || strings.Contains(form.Get("fields"), "slot_number") {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
f.mu.Lock()
|
|
f.Windows = append(f.Windows, form.Get("start"))
|
|
schedule := f.Schedule
|
|
f.mu.Unlock()
|
|
activities := []map[string]any{}
|
|
for offset := range days {
|
|
for _, class := range schedule(start.AddDate(0, 0, offset)) {
|
|
activities = append(activities, map[string]any{
|
|
"type": "classgroup2",
|
|
"start_time": class.Start.Format("2006-01-02 15:04:05"),
|
|
"end_time": class.Start.Add(time.Duration(class.Minutes) * time.Minute).Format("2006-01-02 15:04:05"),
|
|
"name": map[string]string{"pl": class.Kind + " (gr.1)", "en": ""},
|
|
"url": "https://usosweb.example/zajecia?gr_nr=1&zaj_cyk_id=94092",
|
|
"course_id": "S1TK1E>ALzEMD",
|
|
"course_name": map[string]string{"pl": class.Course, "en": "Linear algebra"},
|
|
"classtype_name": map[string]string{"pl": class.Kind, "en": ""},
|
|
"lecturer_ids": []int{141055},
|
|
"group_number": 1,
|
|
"building_name": map[string]string{"pl": "A1 - BM", "en": ""},
|
|
"building_id": "A1",
|
|
"room_number": class.Room,
|
|
"unit_id": 94092,
|
|
"sm_id": class.MeetingID,
|
|
})
|
|
}
|
|
}
|
|
_ = json.NewEncoder(w).Encode(activities)
|
|
}
|