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) }